Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5fa0076f8 | ||
|
|
7a346ef3f6 | ||
|
|
217103f0b6 | ||
|
|
1fbcb65031 | ||
|
|
9e40671798 | ||
|
|
6c8a614872 | ||
|
|
861d9e86ef | ||
|
|
c4b5d3608a | ||
|
|
38e0cc032b | ||
|
|
2c1b8c6f9d | ||
|
|
92f04fbab6 | ||
|
|
515347e29c | ||
|
|
ebefe22a4c | ||
|
|
c93244ee10 | ||
|
|
d84f8a2c88 | ||
|
|
ec40b9d6a2 | ||
|
|
daa16cae99 | ||
|
|
c3bc9e17eb | ||
|
|
c9344fc538 | ||
|
|
d12ad7d900 | ||
|
|
891751043c | ||
|
|
ba447502fe | ||
|
|
c092a7af45 | ||
|
|
34216a9557 | ||
|
|
753283f0e8 | ||
|
|
611456fd49 | ||
|
|
7a1ff0b9ed | ||
|
|
95620285d9 | ||
|
|
8a148899e3 | ||
|
|
fa6c448afa | ||
|
|
cc363e7a6d | ||
|
|
f96c1a2e44 | ||
|
|
856ecdf13d | ||
|
|
f7dac9363a | ||
|
|
b32aabf541 | ||
|
|
945ffe6267 | ||
|
|
17f9b109a2 | ||
|
|
2a24ce03ad | ||
|
|
e34d81be26 | ||
|
|
54e8d61f83 | ||
|
|
35382136c5 | ||
|
|
59d81d4b03 | ||
|
|
06a319c0ee | ||
|
|
a48b003121 | ||
|
|
1620d7a9b0 | ||
|
|
c10069bcc7 | ||
|
|
c0fe7e1305 | ||
|
|
f492109eb7 | ||
|
|
35f3b739d4 | ||
|
|
8f1ca6b854 | ||
|
|
71218996fd | ||
|
|
6bac51dc19 | ||
|
|
b898cad4f4 | ||
|
|
3d172c508a | ||
|
|
48efe74dc0 | ||
|
|
8ae5772c2c | ||
|
|
87cf4fd46d |
+22
@@ -191,3 +191,25 @@ fly.toml
|
|||||||
scripts/deploy-flyio.sh
|
scripts/deploy-flyio.sh
|
||||||
docs/DEPLOYMENT_FLYIO.md
|
docs/DEPLOYMENT_FLYIO.md
|
||||||
setup_jwt_template.py
|
setup_jwt_template.py
|
||||||
|
mcp_server_main.py.backup
|
||||||
|
mcp_overhead_content.json
|
||||||
|
ANTHROPIC_TEST_README.md
|
||||||
|
extract_mcp_overhead.py
|
||||||
|
mcp_overhead_content.txt
|
||||||
|
mcp_overhead_summary.txt
|
||||||
|
run_http_server.py
|
||||||
|
run_local_test.py
|
||||||
|
|
||||||
|
# MCP overhead analysis files
|
||||||
|
mcp_overhead_*.json
|
||||||
|
mcp_overhead_*.txt
|
||||||
|
mcp_test_results_*.json
|
||||||
|
mcp_quick_test_*.json
|
||||||
|
|
||||||
|
# General text files (temporary notes, etc)
|
||||||
|
*.txt
|
||||||
|
analyze_playwright_mcp.py
|
||||||
|
measure_mcp_directly.py
|
||||||
|
playwright_mcp_overhead.json
|
||||||
|
simple_test.py
|
||||||
|
analyze_anayasa_html.py
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
# -------- BASE IMAGE (includes Chromium & deps) ----------------------------
|
# -------- BASE IMAGE (includes Chromium & deps) ----------------------------
|
||||||
FROM mcr.microsoft.com/playwright/python:v1.52.0-noble
|
FROM mcr.microsoft.com/playwright/python:v1.53.0-noble
|
||||||
|
|
||||||
# -------- Runtime setup ----------------------------------------------------
|
# -------- Runtime setup ----------------------------------------------------
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
[](https://www.star-history.com/#saidsurucu/yargi-mcp&Date)
|
[](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, Anayasa Mahkemesi - Norm Denetimi ile Bireysel Başvuru Kararları, Kamu İhale Kurulu Kararları, Rekabet Kurumu Kararları ve Sayıştay 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 veya [5ire](https://5ire.app)) 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ı, Kamu İhale Kurulu Kararları, Rekabet Kurumu Kararları, Sayıştay Kararları, KVKK Kararları ve BDDK 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 veya [5ire](https://5ire.app)) ve diğer istemciler tarafından araç (tool) olarak kullanılabilir hale gelir.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
🎯 **Temel Özellikler**
|
🎯 **Temel Özellikler**
|
||||||
|
|
||||||
|
🚀 **YÜKSEK PERFORMANS OPTİMİZASYONU:** Bu MCP sunucusu **%61.8 token azaltma** ile optimize edilmiştir (8,692 token tasarrufu). Claude AI ile daha hızlı yanıt süreleri ve daha verimli etkileşim sağlar.
|
||||||
|
|
||||||
* Çeşitli Türk hukuk veritabanlarına programatik erişim için standart bir MCP arayüzü.
|
* Çeşitli Türk hukuk veritabanlarına programatik erişim için standart bir MCP arayüzü.
|
||||||
* **Kapsamlı Mahkeme Daire/Kurul Filtreleme:** 79 farklı daire/kurul filtreleme seçeneği
|
* **Kapsamlı Mahkeme Daire/Kurul Filtreleme:** 79 farklı daire/kurul filtreleme seçeneği
|
||||||
* **Dual/Triple API Desteği:** Her mahkeme için birden fazla API kaynağı ile maksimum kapsama
|
* **Dual/Triple API Desteği:** Her mahkeme için birden fazla API kaynağı ile maksimum kapsama
|
||||||
@@ -26,12 +28,15 @@ Bu proje, çeşitli Türk hukuk kaynaklarına (Yargıtay, Danıştay, Emsal Kara
|
|||||||
* **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.
|
* **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.
|
||||||
* **Rekabet Kurumu:** Çeşitli kriterlerle Kurul kararlarını arama; karar metinlerini Markdown formatında getirme.
|
* **Rekabet Kurumu:** Çeşitli kriterlerle Kurul kararlarını arama; karar metinlerini Markdown formatında getirme.
|
||||||
* **Sayıştay:** 3 karar türü ile kapsamlı denetim kararlarına erişim + **8 Daire Filtreleme** + **Tarih Aralığı & İçerik Arama** (Genel Kurul yorumlayıcı kararları, Temyiz Kurulu itiraz kararları, Daire ilk derece denetim kararları)
|
* **Sayıştay:** 3 karar türü ile kapsamlı denetim kararlarına erişim + **8 Daire Filtreleme** + **Tarih Aralığı & İçerik Arama** (Genel Kurul yorumlayıcı kararları, Temyiz Kurulu itiraz kararları, Daire ilk derece denetim kararları)
|
||||||
|
* **KVKK (Kişisel Verilerin Korunması Kurulu):** Brave Search API ile veri koruma kararlarını arama; uzun karar metinlerini (5.000 karakterlik) sayfalanmış Markdown formatında getirme + **Türkçe Arama** + **Site Hedeflemeli Arama** (kvkk.gov.tr kararları)
|
||||||
|
* **BDDK (Bankacılık Düzenleme ve Denetleme Kurumu):** Bankacılık düzenleme kararlarını arama; karar metinlerini Markdown formatında getirme + **Optimized Search** + **"Karar Sayısı" Targeting** + **Spesifik URL Filtreleme** (bddk.org.tr/Mevzuat/DokumanGetir)
|
||||||
|
|
||||||
* Karar metinlerinin daha kolay işlenebilmesi için Markdown formatına çevrilmesi.
|
* Karar metinlerinin daha kolay işlenebilmesi için Markdown formatına çevrilmesi.
|
||||||
* Claude Desktop uygulaması ile `fastmcp install` komutu kullanılarak kolay entegrasyon.
|
* Claude Desktop uygulaması ile `fastmcp install` komutu kullanılarak kolay entegrasyon.
|
||||||
* Yargı MCP artık [5ire](https://5ire.app) gibi Claude Desktop haricindeki MCP istemcilerini de destekliyor!
|
* Yargı MCP artık [5ire](https://5ire.app) gibi Claude Desktop haricindeki MCP istemcilerini de destekliyor!
|
||||||
---
|
---
|
||||||
🚀 **Claude Haricindeki Modellerle Kullanmak İçin Çok Kolay Kurulum (Örnek: 5ire için)**
|
<details>
|
||||||
|
<summary>🚀 <strong>Claude Haricindeki Modellerle Kullanmak İçin Çok Kolay Kurulum (Örnek: 5ire için)</strong></summary>
|
||||||
|
|
||||||
Bu bölüm, Yargı MCP aracını 5ire gibi Claude Desktop dışındaki MCP istemcileriyle kullanmak isteyenler içindir.
|
Bu bölüm, Yargı MCP aracını 5ire gibi Claude Desktop dışındaki MCP istemcileriyle kullanmak isteyenler içindir.
|
||||||
|
|
||||||
@@ -55,9 +60,11 @@ Bu bölüm, Yargı MCP aracını 5ire gibi Claude Desktop dışındaki MCP istem
|
|||||||
* Şimdi **Tools** altında **Yargı MCP**'yi görüyor olmalısınız. Üstüne geldiğinizde sağda çıkan butona tıklayıp etkinleştirin (yeşil ışık yanmalı).
|
* Şimdi **Tools** altında **Yargı MCP**'yi görüyor olmalısınız. Üstüne geldiğinizde sağda çıkan butona tıklayıp etkinleştirin (yeşil ışık yanmalı).
|
||||||
* Artık Yargı MCP ile konuşabilirsiniz.
|
* Artık Yargı MCP ile konuşabilirsiniz.
|
||||||
|
|
||||||
---
|
</details>
|
||||||
⚙️ **Claude Desktop Manuel Kurulumu**
|
|
||||||
|
|
||||||
|
---
|
||||||
|
<details>
|
||||||
|
<summary>⚙️ <strong>Claude Desktop Manuel Kurulumu</strong></summary>
|
||||||
|
|
||||||
1. **Ön Gereksinimler:** Python, `uv`, (Windows için) Microsoft Visual C++ Redistributable'ın sisteminizde kurulu olduğundan emin olun. Detaylı bilgi için yukarıdaki "5ire için Kurulum" bölümündeki ilgili adımlara bakabilirsiniz.
|
1. **Ön Gereksinimler:** Python, `uv`, (Windows için) Microsoft Visual C++ Redistributable'ın sisteminizde kurulu olduğundan emin olun. Detaylı bilgi için yukarıdaki "5ire için Kurulum" bölümündeki ilgili adımlara bakabilirsiniz.
|
||||||
2. Claude Desktop **Settings -> Developer -> Edit Config**.
|
2. Claude Desktop **Settings -> Developer -> Edit Config**.
|
||||||
@@ -78,8 +85,11 @@ Bu bölüm, Yargı MCP aracını 5ire gibi Claude Desktop dışındaki MCP istem
|
|||||||
```
|
```
|
||||||
4. Claude Desktop'ı kapatıp yeniden başlatın.
|
4. Claude Desktop'ı kapatıp yeniden başlatın.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
---
|
---
|
||||||
🌟 **Gemini CLI ile Kullanım**
|
<details>
|
||||||
|
<summary>🌟 <strong>Gemini CLI ile Kullanım</strong></summary>
|
||||||
|
|
||||||
Yargı MCP'yi Gemini CLI ile kullanmak için:
|
Yargı MCP'yi Gemini CLI ile kullanmak için:
|
||||||
|
|
||||||
@@ -120,58 +130,39 @@ Yargı MCP'yi Gemini CLI ile kullanmak için:
|
|||||||
- "Danıştay'ın imar planı iptaline ilişkin kararlarını bul"
|
- "Danıştay'ın imar planı iptaline ilişkin kararlarını bul"
|
||||||
- "Anayasa Mahkemesi'nin ifade özgürlüğü kararlarını getir"
|
- "Anayasa Mahkemesi'nin ifade özgürlüğü kararlarını getir"
|
||||||
|
|
||||||
🛠️ **Kullanılabilir Araçlar (MCP Tools)**
|
</details>
|
||||||
|
|
||||||
Bu FastMCP sunucusu aşağıdaki temel araçları sunar:
|
<details>
|
||||||
|
<summary>🛠️ <strong>Kullanılabilir Araçlar (MCP Tools)</strong></summary>
|
||||||
|
|
||||||
### **Yargıtay Araçları (Dual API + 52 Daire Filtreleme)**
|
Bu FastMCP sunucusu **19 optimize edilmiş MCP aracı** sunar (token verimliliği için optimize edilmiş):
|
||||||
* **Ana API:**
|
|
||||||
* `search_yargitay_detailed(arananKelime, birimYrgKurulDaire, ...)`: Yargıtay kararlarını detaylı kriterlerle arar. **52 daire/kurul seçeneği** (Hukuk/Ceza Daireleri 1-23, Genel Kurullar, Başkanlar Kurulu)
|
|
||||||
* `get_yargitay_document_markdown(id: str)`: Belirli bir Yargıtay kararının metnini Markdown formatında getirir.
|
|
||||||
* **Bedesten API (Alternatif):**
|
|
||||||
* `search_yargitay_bedesten(phrase, birimAdi, kararTarihiStart, kararTarihiEnd, ...)`: Bedesten API ile Yargıtay kararlarını arar. **Aynı 52 daire filtreleme** + **Tarih Filtreleme** + **Kesin Cümle Arama** (`"\"mülkiyet kararı\""`)
|
|
||||||
* `get_yargitay_bedesten_document_markdown(documentId: str)`: Bedesten'den karar metni (HTML/PDF → Markdown)
|
|
||||||
|
|
||||||
### **Danıştay Araçları (Triple API + 27 Daire Filtreleme)**
|
### **Yargıtay Araçları (Birleşik Bedesten API - Token Optimized)**
|
||||||
* **Ana API'lar:**
|
*Not: Yargıtay araçları token verimliliği için birleşik Bedesten API'ye entegre edilmiştir*
|
||||||
* `search_danistay_by_keyword(andKelimeler, orKelimeler, ...)`: Danıştay kararlarını anahtar kelimelerle arar.
|
|
||||||
* `search_danistay_detailed(daire, esasYil, ...)`: Danıştay kararlarını detaylı kriterlerle arar.
|
|
||||||
* `get_danistay_document_markdown(id: str)`: Belirli bir Danıştay kararının metnini Markdown formatında getirir.
|
|
||||||
* **Bedesten API (Alternatif):**
|
|
||||||
* `search_danistay_bedesten(phrase, birimAdi, kararTarihiStart, kararTarihiEnd, ...)`: Bedesten API ile Danıştay kararlarını arar. **27 daire/kurul seçeneği** + **Tarih Filtreleme** + **Kesin Cümle Arama** (`"\"idari işlem\""`) (1-17. Daireler, Vergi/İdare Kurulları, Askeri Mahkemeler)
|
|
||||||
* `get_danistay_bedesten_document_markdown(documentId: str)`: Bedesten'den karar metni
|
|
||||||
|
|
||||||
### **Diğer Mahkemeler (Bedesten API + Gelişmiş Arama)**
|
### **Danıştay Araçları (Birleşik Bedesten API - Token Optimized)**
|
||||||
* **Yerel Hukuk Mahkemeleri:**
|
*Not: Danıştay araçları token verimliliği için birleşik Bedesten API'ye entegre edilmiştir*
|
||||||
* `search_yerel_hukuk_bedesten(phrase, kararTarihiStart, kararTarihiEnd, ...)`: Yerel hukuk mahkemesi kararlarını arar + **Tarih & Kesin Cümle Arama** (`"\"sözleşme ihlali\""`)
|
|
||||||
* `get_yerel_hukuk_bedesten_document_markdown(documentId: str)`: Karar metni
|
|
||||||
* **İstinaf Hukuk Mahkemeleri:**
|
|
||||||
* `search_istinaf_hukuk_bedesten(phrase, kararTarihiStart, kararTarihiEnd, ...)`: İstinaf mahkemesi kararlarını arar + **Tarih & Kesin Cümle Arama** (`"\"temyiz incelemesi\""`)
|
|
||||||
* `get_istinaf_hukuk_bedesten_document_markdown(documentId: str)`: Karar metni
|
|
||||||
* **Kanun Yararına Bozma (KYB):**
|
|
||||||
* `search_kyb_bedesten(phrase, kararTarihiStart, kararTarihiEnd, ...)`: Olağanüstü kanun yolu kararlarını arar + **Tarih & Kesin Cümle Arama** (`"\"kanun yararına bozma\""`)
|
|
||||||
* `get_kyb_bedesten_document_markdown(documentId: str)`: Karar metni
|
|
||||||
|
|
||||||
* **Emsal Karar Araçları:**
|
### **Birleşik Bedesten API Araçları (5 Mahkeme) - 🚀 TOKEN OPTİMİZE**
|
||||||
* `search_emsal_detailed_decisions(search_query: EmsalSearchRequest) -> CompactEmsalSearchResult`: Emsal (UYAP) kararlarını detaylı kriterlerle arar.
|
1. `search_bedesten_unified(phrase, court_types, birimAdi, kararTarihiStart, kararTarihiEnd, ...)`: **5 mahkeme türünü** birleşik arama (Yargıtay, Danıştay, Yerel Hukuk, İstinaf Hukuk, KYB) + **79 daire filtreleme** + **Tarih & Kesin Cümle Arama**
|
||||||
* `get_emsal_document_markdown(id: str) -> EmsalDocumentMarkdown`: Belirli bir Emsal kararının metnini Markdown formatında getirir.
|
2. `get_bedesten_document_markdown(documentId: str)`: Bedesten API'den herhangi bir belgeyi Markdown formatında getirir (HTML/PDF → Markdown)
|
||||||
|
|
||||||
* **Uyuşmazlık Mahkemesi Araçları:**
|
### **Emsal Karar Araçları (UYAP)**
|
||||||
* `search_uyusmazlik_decisions(search_params: UyusmazlikSearchRequest) -> UyusmazlikSearchResponse`: Uyuşmazlık Mahkemesi kararlarını çeşitli form kriterleriyle arar.
|
3. `search_emsal_detailed_decisions(keyword, ...)`: Emsal (UYAP) kararlarını detaylı kriterlerle arar.
|
||||||
* `get_uyusmazlik_document_markdown_from_url(document_url: HttpUrl) -> UyusmazlikDocumentMarkdown`: Bir Uyuşmazlık kararını tam URL'sinden alıp Markdown formatında getirir.
|
4. `get_emsal_document_markdown(id: str)`: Belirli bir Emsal kararının metnini Markdown formatında getirir.
|
||||||
|
|
||||||
* **Anayasa Mahkemesi (Norm Denetimi) Araçları:**
|
### **Uyuşmazlık Mahkemesi Araçları**
|
||||||
* `search_anayasa_norm_denetimi_decisions(search_query: AnayasaNormDenetimiSearchRequest) -> AnayasaSearchResult`: AYM Norm Denetimi kararlarını kapsamlı kriterlerle arar.
|
5. `search_uyusmazlik_decisions(icerik, ...)`: Uyuşmazlık Mahkemesi kararlarını çeşitli form kriterleriyle arar.
|
||||||
* `get_anayasa_norm_denetimi_document_markdown(document_url: str, page_number: Optional[int] = 1) -> AnayasaDocumentMarkdown`: Belirli bir AYM Norm Denetimi kararını URL'sinden alır ve 5.000 karakterlik sayfalanmış Markdown içeriğini getirir.
|
6. `get_uyusmazlik_document_markdown_from_url(document_url)`: Bir Uyuşmazlık kararını tam URL'sinden alıp Markdown formatında getirir.
|
||||||
|
|
||||||
* **Anayasa Mahkemesi (Bireysel Başvuru) Araçları:**
|
### **Anayasa Mahkemesi Araçları (Birleşik API) - 🚀 TOKEN OPTİMİZE**
|
||||||
* `search_anayasa_bireysel_basvuru_report(search_query: AnayasaBireyselReportSearchRequest) -> AnayasaBireyselReportSearchResult`: AYM Bireysel Başvuru "Karar Arama Raporu" oluşturur.
|
7. `search_anayasa_unified(decision_type, keywords_all, ...)`: AYM kararlarını birleşik arama (Norm Denetimi + Bireysel Başvuru) - **4 araç → 2 araç optimizasyonu**
|
||||||
* `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.
|
8. `get_anayasa_document_unified(document_url, page_number)`: AYM kararlarını birleşik belge getirme - **sayfalanmış Markdown** içeriği
|
||||||
|
|
||||||
* **KİK (Kamu İhale Kurulu) Araçları:**
|
### **KİK (Kamu İhale Kurulu) Araçları**
|
||||||
* `search_kik_decisions(search_query: KikSearchRequest) -> KikSearchResult`: KİK (Kamu İhale Kurulu) kararlarını arar.
|
9. `search_kik_decisions(karar_tipi, ...)`: 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.
|
10. `get_kik_document_markdown(karar_id, page_number)`: Belirli bir KİK kararını, Base64 ile encode edilmiş `karar_id`'sini kullanarak alır ve **sayfalanmış Markdown** içeriğini getirir.
|
||||||
* **Rekabet Kurumu Araçları:**
|
### **Rekabet Kurumu Araçları**
|
||||||
* `search_rekabet_kurumu_decisions(KararTuru: Literal[...], ...) -> RekabetSearchResult`: Rekabet Kurumu kararlarını arar. `KararTuru` için kullanıcı dostu isimler kullanılır (örn: "Birleşme ve Devralma").
|
* `search_rekabet_kurumu_decisions(KararTuru: Literal[...], ...) -> RekabetSearchResult`: Rekabet Kurumu kararlarını arar. `KararTuru` için kullanıcı dostu isimler kullanılır (örn: "Birleşme ve Devralma").
|
||||||
* `get_rekabet_kurumu_document(karar_id: str, page_number: Optional[int] = 1) -> RekabetDocument`: Belirli bir Rekabet Kurumu kararını `karar_id` ile alır. Kararın PDF formatındaki orijinalinden istenen sayfayı ayıklar ve Markdown formatında döndürür.
|
* `get_rekabet_kurumu_document(karar_id: str, page_number: Optional[int] = 1) -> RekabetDocument`: Belirli bir Rekabet Kurumu kararını `karar_id` ile alır. Kararın PDF formatındaki orijinalinden istenen sayfayı ayıklar ve Markdown formatında döndürür.
|
||||||
|
|
||||||
@@ -186,15 +177,34 @@ Bu FastMCP sunucusu aşağıdaki temel araçları sunar:
|
|||||||
* `get_sayistay_temyiz_kurulu_document_markdown(decision_id: str)`: Temyiz Kurulu kararının tam metnini Markdown formatında getirir
|
* `get_sayistay_temyiz_kurulu_document_markdown(decision_id: str)`: Temyiz Kurulu kararının tam metnini Markdown formatında getirir
|
||||||
* `get_sayistay_daire_document_markdown(decision_id: str)`: Daire kararının tam metnini Markdown formatında getirir
|
* `get_sayistay_daire_document_markdown(decision_id: str)`: Daire kararının tam metnini Markdown formatında getirir
|
||||||
|
|
||||||
|
* **KVKK Araçları (Brave Search API + Türkçe Arama):**
|
||||||
|
* `search_kvkk_decisions(keywords, page, pageSize, ...)`: KVKK (Kişisel Verilerin Korunması Kurulu) kararlarını Brave Search API ile arar. **Türkçe arama** + **Site hedeflemeli** (`site:kvkk.gov.tr "karar özeti"`) + **Sayfalama desteği**
|
||||||
|
* `get_kvkk_document_markdown(decision_url: str, page_number: Optional[int] = 1)`: KVKK kararının tam metnini **sayfalanmış Markdown** formatında getirir (5.000 karakterlik sayfa)
|
||||||
|
|
||||||
|
### BDDK Araçları
|
||||||
|
* `search_bddk_decisions(keywords, page)`: BDDK (Bankacılık Düzenleme ve Denetleme Kurumu) kararlarını arar. **"Karar Sayısı" targeting** + **Spesifik URL filtreleme** (`bddk.org.tr/Mevzuat/DokumanGetir`) + **Optimized search**
|
||||||
|
* `get_bddk_document_markdown(document_id: str, page_number: Optional[int] = 1)`: BDDK kararının tam metnini **sayfalanmış Markdown** formatında getirir (5.000 karakterlik sayfa)
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### **📊 Kapsamlı İstatistikler**
|
<details>
|
||||||
- **Toplam Mahkeme/Kurum:** 12 farklı hukuki kurum
|
<summary>📊 <strong>Kapsamlı İstatistikler & Optimizasyon Başarıları</strong></summary>
|
||||||
- **Toplam MCP Tool:** 36+ arama ve belge getirme aracı
|
|
||||||
|
🚀 **TOKEN OPTİMİZASYON BAŞARISI:**
|
||||||
|
- **%61.8 Token Azaltma:** 14,061 → 5,369 tokens (8,692 token tasarrufu)
|
||||||
|
- **Hedef Aşım:** 10,000 token hedefini 4,631 token aştık
|
||||||
|
- **Daha Hızlı Yanıt:** Claude AI ile optimize edilmiş etkileşim
|
||||||
|
- **Korunan İşlevsellik:** %100 özellik desteği devam ediyor
|
||||||
|
|
||||||
|
**GENEL İSTATİSTİKLER:**
|
||||||
|
- **Toplam Mahkeme/Kurum:** 13 farklı hukuki kurum (KVKK dahil)
|
||||||
|
- **Toplam MCP Tool:** 19 optimize edilmiş arama ve belge getirme aracı
|
||||||
- **Daire/Kurul Filtreleme:** 87 farklı seçenek (52 Yargıtay + 27 Danıştay + 8 Sayıştay)
|
- **Daire/Kurul Filtreleme:** 87 farklı seçenek (52 Yargıtay + 27 Danıştay + 8 Sayıştay)
|
||||||
- **Tarih Filtreleme:** 5 Bedesten API aracında ISO 8601 formatında tam tarih aralığı desteği
|
- **Tarih Filtreleme:** Birleşik Bedesten API aracında ISO 8601 formatında tam tarih aralığı desteği
|
||||||
- **Kesin Cümle Arama:** 5 Bedesten API aracında çift tırnak ile tam cümle arama (`"\"mülkiyet kararı\""` formatı)
|
- **Kesin Cümle Arama:** Birleşik Bedesten API aracında çift tırnak ile tam cümle arama (`"\"mülkiyet kararı\""` formatı)
|
||||||
|
- **Birleşik API:** 10 ayrı Bedesten aracı → 2 birleşik araç (search_bedesten_unified + get_bedesten_document_markdown)
|
||||||
- **API Kaynağı:** Dual/Triple API desteği ile maksimum kapsama
|
- **API Kaynağı:** Dual/Triple API desteği ile maksimum kapsama
|
||||||
- **Tam Türk Adalet Sistemi:** Yerel mahkemelerden en yüksek mahkemelere kadar
|
- **Tam Türk Adalet Sistemi:** Yerel mahkemelerden en yüksek mahkemelere kadar
|
||||||
|
|
||||||
@@ -219,9 +229,19 @@ Bedesten API Bedesten API Dual/Triple API Norm+Bireysel API
|
|||||||
- Kesin arama: `"\"mülkiyet kararı\""` (tam cümle olarak)
|
- Kesin arama: `"\"mülkiyet kararı\""` (tam cümle olarak)
|
||||||
- Daha kesin sonuçlar için hukuki terimler ve kavramlar
|
- Daha kesin sonuçlar için hukuki terimler ve kavramlar
|
||||||
|
|
||||||
|
**🔧 OPTİMİZASYON DETAYLARI:**
|
||||||
|
- **Anayasa Mahkemesi:** 4 araç → 2 birleşik araç (search_anayasa_unified + get_anayasa_document_unified)
|
||||||
|
- **Yargıtay & Danıştay:** Ana API araçları birleşik Bedesten API'ye entegre edildi
|
||||||
|
- **Sayıştay:** 6 araç → 2 birleşik araç (search_sayistay_unified + get_sayistay_document_unified)
|
||||||
|
- **Parameter Optimizasyonu:** pageSize parametreleri optimize edildi
|
||||||
|
- **Açıklama Optimizasyonu:** Uzun açıklamalar kısaltıldı (örn: KIK karar_metni)
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
🌐 **Web Service / ASGI Deployment**
|
<details>
|
||||||
|
<summary>🌐 <strong>Web Service / ASGI Deployment</strong></summary>
|
||||||
|
|
||||||
Yargı MCP artık web servisi olarak da çalıştırılabilir! ASGI desteği sayesinde:
|
Yargı MCP artık web servisi olarak da çalıştırılabilir! ASGI desteği sayesinde:
|
||||||
|
|
||||||
@@ -243,6 +263,8 @@ uvicorn asgi_app:app --host 0.0.0.0 --port 8000
|
|||||||
|
|
||||||
Detaylı deployment rehberi için: [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md)
|
Detaylı deployment rehberi için: [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md)
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
📜 **Lisans**
|
📜 **Lisans**
|
||||||
|
|||||||
@@ -99,11 +99,11 @@ class AnayasaBireyselBasvuruApiClient:
|
|||||||
|
|
||||||
for decision_div in decision_divs:
|
for decision_div in decision_divs:
|
||||||
title_tag = decision_div.find("h4")
|
title_tag = decision_div.find("h4")
|
||||||
title_text = title_tag.get_text(strip=True) if title_tag and title_tag.strong else (title_tag.get_text(strip=True) if title_tag else None)
|
title_text = title_tag.get_text(strip=True) if title_tag and title_tag.strong else (title_tag.get_text(strip=True) if title_tag else "")
|
||||||
|
|
||||||
|
|
||||||
alti_cizili_div = decision_div.find("div", class_="AltiCizili")
|
alti_cizili_div = decision_div.find("div", class_="AltiCizili")
|
||||||
ref_no, dec_type, body, app_date, dec_date, url_path = None, None, None, None, None, None
|
ref_no, dec_type, body, app_date, dec_date, url_path = "", "", "", "", "", ""
|
||||||
if alti_cizili_div:
|
if alti_cizili_div:
|
||||||
link_tag = alti_cizili_div.find("a", href=True)
|
link_tag = alti_cizili_div.find("a", href=True)
|
||||||
if link_tag:
|
if link_tag:
|
||||||
@@ -124,14 +124,14 @@ class AnayasaBireyselBasvuruApiClient:
|
|||||||
ref_no = parts[current_idx]
|
ref_no = parts[current_idx]
|
||||||
current_idx += 1
|
current_idx += 1
|
||||||
|
|
||||||
dec_type = parts[current_idx] if len(parts) > current_idx else None
|
dec_type = parts[current_idx] if len(parts) > current_idx else ""
|
||||||
current_idx += 1
|
current_idx += 1
|
||||||
body = parts[current_idx] if len(parts) > current_idx else None
|
body = parts[current_idx] if len(parts) > current_idx else ""
|
||||||
current_idx += 1
|
current_idx += 1
|
||||||
|
|
||||||
app_date_raw = parts[current_idx] if len(parts) > current_idx else None
|
app_date_raw = parts[current_idx] if len(parts) > current_idx else ""
|
||||||
current_idx += 1
|
current_idx += 1
|
||||||
dec_date_raw = parts[current_idx] if len(parts) > current_idx else None
|
dec_date_raw = parts[current_idx] if len(parts) > current_idx else ""
|
||||||
|
|
||||||
if app_date_raw and "Başvuru Tarihi :" in app_date_raw:
|
if app_date_raw and "Başvuru Tarihi :" in app_date_raw:
|
||||||
app_date = app_date_raw.replace("Başvuru Tarihi :", "").strip()
|
app_date = app_date_raw.replace("Başvuru Tarihi :", "").strip()
|
||||||
@@ -148,7 +148,7 @@ class AnayasaBireyselBasvuruApiClient:
|
|||||||
|
|
||||||
|
|
||||||
subject_div = decision_div.find(lambda tag: tag.name == 'div' and not tag.has_attr('class') and tag.get_text(strip=True).startswith("BAŞVURU KONUSU :"))
|
subject_div = decision_div.find(lambda tag: tag.name == 'div' and not tag.has_attr('class') and tag.get_text(strip=True).startswith("BAŞVURU KONUSU :"))
|
||||||
subject_text = subject_div.get_text(strip=True).replace("BAŞVURU KONUSU :", "").strip() if subject_div else None
|
subject_text = subject_div.get_text(strip=True).replace("BAŞVURU KONUSU :", "").strip() if subject_div else ""
|
||||||
|
|
||||||
details_list: List[AnayasaBireyselReportDecisionDetail] = []
|
details_list: List[AnayasaBireyselReportDecisionDetail] = []
|
||||||
karar_detaylari_div = decision_div.find_next_sibling("div", id="KararDetaylari") # Corrected: was KararDetaylari
|
karar_detaylari_div = decision_div.find_next_sibling("div", id="KararDetaylari") # Corrected: was KararDetaylari
|
||||||
@@ -159,13 +159,13 @@ class AnayasaBireyselBasvuruApiClient:
|
|||||||
cells = row.find_all("td")
|
cells = row.find_all("td")
|
||||||
if len(cells) == 4: # Hak, Müdahale İddiası, Sonuç, Giderim
|
if len(cells) == 4: # Hak, Müdahale İddiası, Sonuç, Giderim
|
||||||
details_list.append(AnayasaBireyselReportDecisionDetail(
|
details_list.append(AnayasaBireyselReportDecisionDetail(
|
||||||
hak=cells[0].get_text(strip=True) or None,
|
hak=cells[0].get_text(strip=True) or "",
|
||||||
mudahale_iddiasi=cells[1].get_text(strip=True) or None,
|
mudahale_iddiasi=cells[1].get_text(strip=True) or "",
|
||||||
sonuc=cells[2].get_text(strip=True) or None,
|
sonuc=cells[2].get_text(strip=True) or "",
|
||||||
giderim=cells[3].get_text(strip=True) or None,
|
giderim=cells[3].get_text(strip=True) or "",
|
||||||
))
|
))
|
||||||
|
|
||||||
full_decision_page_url = urljoin(self.BASE_URL, url_path) if url_path else None
|
full_decision_page_url = urljoin(self.BASE_URL, url_path) if url_path else ""
|
||||||
|
|
||||||
processed_decisions.append(AnayasaBireyselReportDecisionSummary(
|
processed_decisions.append(AnayasaBireyselReportDecisionSummary(
|
||||||
title=title_text,
|
title=title_text,
|
||||||
|
|||||||
@@ -50,36 +50,36 @@ class AnayasaMahkemesiApiClient:
|
|||||||
for kw in params.keywords_any: query_params.append(("HerhangiBirKelimeAra[]", kw))
|
for kw in params.keywords_any: query_params.append(("HerhangiBirKelimeAra[]", kw))
|
||||||
if params.keywords_exclude:
|
if params.keywords_exclude:
|
||||||
for kw in params.keywords_exclude: query_params.append(("BulunmayanKelimeAra[]", kw))
|
for kw in params.keywords_exclude: query_params.append(("BulunmayanKelimeAra[]", kw))
|
||||||
if params.period and params.period.value and params.period.value != "ALL": query_params.append(("Donemler_id", params.period.value))
|
if params.period and params.period and params.period != "ALL": query_params.append(("Donemler_id", params.period))
|
||||||
if params.case_number_esas: query_params.append(("EsasNo", params.case_number_esas))
|
if params.case_number_esas: query_params.append(("EsasNo", params.case_number_esas))
|
||||||
if params.decision_number_karar: query_params.append(("KararNo", params.decision_number_karar))
|
if params.decision_number_karar: query_params.append(("KararNo", params.decision_number_karar))
|
||||||
if params.first_review_date_start: query_params.append(("IlkIncelemeTarihiIlk", params.first_review_date_start))
|
if params.first_review_date_start: query_params.append(("IlkIncelemeTarihiIlk", params.first_review_date_start))
|
||||||
if params.first_review_date_end: query_params.append(("IlkIncelemeTarihiSon", params.first_review_date_end))
|
if params.first_review_date_end: query_params.append(("IlkIncelemeTarihiSon", params.first_review_date_end))
|
||||||
if params.decision_date_start: query_params.append(("KararTarihiIlk", params.decision_date_start))
|
if params.decision_date_start: query_params.append(("KararTarihiIlk", params.decision_date_start))
|
||||||
if params.decision_date_end: query_params.append(("KararTarihiSon", params.decision_date_end))
|
if params.decision_date_end: query_params.append(("KararTarihiSon", params.decision_date_end))
|
||||||
if params.application_type and params.application_type.value and params.application_type.value != "ALL": query_params.append(("BasvuruTurler_id", params.application_type.value))
|
if params.application_type and params.application_type and params.application_type != "ALL": query_params.append(("BasvuruTurler_id", params.application_type))
|
||||||
if params.applicant_general_name: query_params.append(("BasvuranGeneller_id", params.applicant_general_name))
|
if params.applicant_general_name: query_params.append(("BasvuranGeneller_id", params.applicant_general_name))
|
||||||
if params.applicant_specific_name: query_params.append(("BasvuranOzeller_id", params.applicant_specific_name))
|
if params.applicant_specific_name: query_params.append(("BasvuranOzeller_id", params.applicant_specific_name))
|
||||||
if params.attending_members_names:
|
if params.attending_members_names:
|
||||||
for name in params.attending_members_names: query_params.append(("Uyeler_id[]", name))
|
for name in params.attending_members_names: query_params.append(("Uyeler_id[]", name))
|
||||||
if params.rapporteur_name: query_params.append(("Raportorler_id", params.rapporteur_name))
|
if params.rapporteur_name: query_params.append(("Raportorler_id", params.rapporteur_name))
|
||||||
if params.norm_type and params.norm_type.value and params.norm_type.value != "ALL": query_params.append(("NormunTurler_id", params.norm_type.value))
|
if params.norm_type and params.norm_type and params.norm_type != "ALL": query_params.append(("NormunTurler_id", params.norm_type))
|
||||||
if params.norm_id_or_name: query_params.append(("NormunNumarasiAdlar_id", params.norm_id_or_name))
|
if params.norm_id_or_name: query_params.append(("NormunNumarasiAdlar_id", params.norm_id_or_name))
|
||||||
if params.norm_article: query_params.append(("NormunMaddeNumarasi", params.norm_article))
|
if params.norm_article: query_params.append(("NormunMaddeNumarasi", params.norm_article))
|
||||||
if params.review_outcomes:
|
if params.review_outcomes:
|
||||||
for outcome_enum_val in params.review_outcomes:
|
for outcome_val in params.review_outcomes:
|
||||||
if outcome_enum_val.value and outcome_enum_val.value != "ALL": query_params.append(("IncelemeTuruKararSonuclar_id[]", outcome_enum_val.value))
|
if outcome_val and outcome_val != "ALL": query_params.append(("IncelemeTuruKararSonuclar_id[]", outcome_val))
|
||||||
if params.reason_for_final_outcome and params.reason_for_final_outcome.value and params.reason_for_final_outcome.value != "ALL":
|
if params.reason_for_final_outcome and params.reason_for_final_outcome and params.reason_for_final_outcome != "ALL":
|
||||||
query_params.append(("KararSonucununGerekcesi", params.reason_for_final_outcome.value))
|
query_params.append(("KararSonucununGerekcesi", params.reason_for_final_outcome))
|
||||||
if params.basis_constitution_article_numbers:
|
if params.basis_constitution_article_numbers:
|
||||||
for article_no in params.basis_constitution_article_numbers: query_params.append(("DayanakHukmu[]", article_no))
|
for article_no in params.basis_constitution_article_numbers: query_params.append(("DayanakHukmu[]", article_no))
|
||||||
if params.official_gazette_date_start: query_params.append(("ResmiGazeteTarihiIlk", params.official_gazette_date_start))
|
if params.official_gazette_date_start: query_params.append(("ResmiGazeteTarihiIlk", params.official_gazette_date_start))
|
||||||
if params.official_gazette_date_end: query_params.append(("ResmiGazeteTarihiSon", params.official_gazette_date_end))
|
if params.official_gazette_date_end: query_params.append(("ResmiGazeteTarihiSon", params.official_gazette_date_end))
|
||||||
if params.official_gazette_number_start: query_params.append(("ResmiGazeteSayisiIlk", params.official_gazette_number_start))
|
if params.official_gazette_number_start: query_params.append(("ResmiGazeteSayisiIlk", params.official_gazette_number_start))
|
||||||
if params.official_gazette_number_end: query_params.append(("ResmiGazeteSayisiSon", params.official_gazette_number_end))
|
if params.official_gazette_number_end: query_params.append(("ResmiGazeteSayisiSon", params.official_gazette_number_end))
|
||||||
if params.has_press_release and params.has_press_release.value and params.has_press_release.value != "ALL": query_params.append(("BasinDuyurusu", params.has_press_release.value))
|
if params.has_press_release and params.has_press_release and params.has_press_release != "ALL": query_params.append(("BasinDuyurusu", params.has_press_release))
|
||||||
if params.has_dissenting_opinion and params.has_dissenting_opinion.value and params.has_dissenting_opinion.value != "ALL": query_params.append(("KarsiOy", params.has_dissenting_opinion.value))
|
if params.has_dissenting_opinion and params.has_dissenting_opinion and params.has_dissenting_opinion != "ALL": query_params.append(("KarsiOy", params.has_dissenting_opinion))
|
||||||
if params.has_different_reasoning and params.has_different_reasoning.value and params.has_different_reasoning.value != "ALL": query_params.append(("FarkliGerekce", params.has_different_reasoning.value))
|
if params.has_different_reasoning and params.has_different_reasoning and params.has_different_reasoning != "ALL": query_params.append(("FarkliGerekce", params.has_different_reasoning))
|
||||||
|
|
||||||
# Add pagination and sorting parameters as query params instead of URL path
|
# Add pagination and sorting parameters as query params instead of URL path
|
||||||
if params.results_per_page and params.results_per_page != 10:
|
if params.results_per_page and params.results_per_page != 10:
|
||||||
@@ -274,6 +274,11 @@ class AnayasaMahkemesiApiClient:
|
|||||||
if not karar_metni_div: # Fallback if not in KararMetni
|
if not karar_metni_div: # Fallback if not in KararMetni
|
||||||
karar_metni_div = soup.find("div", class_="WordSection1")
|
karar_metni_div = soup.find("div", class_="WordSection1")
|
||||||
|
|
||||||
|
# Initialize with empty string defaults
|
||||||
|
decision_ek_no_from_page = ""
|
||||||
|
decision_date_from_page = ""
|
||||||
|
official_gazette_from_page = ""
|
||||||
|
|
||||||
if karar_metni_div:
|
if karar_metni_div:
|
||||||
# Attempt to find E.K. No (Esas No, Karar No)
|
# Attempt to find E.K. No (Esas No, Karar No)
|
||||||
# Norm Denetimi pages often have this in bold <p> tags directly or in the WordSection1
|
# Norm Denetimi pages often have this in bold <p> tags directly or in the WordSection1
|
||||||
|
|||||||
+100
-82
@@ -1,43 +1,21 @@
|
|||||||
# anayasa_mcp_module/models.py
|
# anayasa_mcp_module/models.py
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, HttpUrl
|
from pydantic import BaseModel, Field, HttpUrl
|
||||||
from typing import List, Optional, Dict, Any
|
from typing import List, Optional, Dict, Any, Literal
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
# --- Enums (AnayasaDonemEnum, AnayasaBasvuruTuruEnum, etc. - same as before) ---
|
# --- Enums (AnayasaDonemEnum, etc. - same as before) ---
|
||||||
class AnayasaDonemEnum(str, Enum):
|
class AnayasaDonemEnum(str, Enum):
|
||||||
TUMU = "ALL"
|
TUMU = "ALL"
|
||||||
DONEM_1961 = "1"
|
DONEM_1961 = "1"
|
||||||
DONEM_1982 = "2"
|
DONEM_1982 = "2"
|
||||||
|
|
||||||
class AnayasaBasvuruTuruEnum(str, Enum):
|
|
||||||
TUMU = "ALL"
|
|
||||||
IPTAL = "1"
|
|
||||||
ITIRAZ = "2"
|
|
||||||
DIGER = "3"
|
|
||||||
|
|
||||||
class AnayasaVarYokEnum(str, Enum):
|
class AnayasaVarYokEnum(str, Enum):
|
||||||
TUMU = "ALL"
|
TUMU = "ALL"
|
||||||
YOK = "0"
|
YOK = "0"
|
||||||
VAR = "1"
|
VAR = "1"
|
||||||
|
|
||||||
class AnayasaNormTuruEnum(str, Enum):
|
|
||||||
TUMU = "ALL"
|
|
||||||
ANAYASA = "1"
|
|
||||||
ANAYASA_DEGISTIREN_KANUN = "2"
|
|
||||||
CUMHURBASKANLIGI_KARARNAMESI = "14"
|
|
||||||
ICTUZUK = "3"
|
|
||||||
KANUN = "4"
|
|
||||||
KANUN_HUKMUNDE_KARARNAME = "5"
|
|
||||||
KARAR = "6"
|
|
||||||
NIZAMNAME = "7"
|
|
||||||
TALIMATNAME = "8"
|
|
||||||
TARIFE = "9"
|
|
||||||
TBMM_KARARI = "10"
|
|
||||||
TEZKERE = "11"
|
|
||||||
TUZUK = "12"
|
|
||||||
YOK_SECENEGI = "0"
|
|
||||||
YONETMELIK = "13"
|
|
||||||
|
|
||||||
class AnayasaIncelemeSonucuEnum(str, Enum):
|
class AnayasaIncelemeSonucuEnum(str, Enum):
|
||||||
TUMU = "ALL"
|
TUMU = "ALL"
|
||||||
@@ -89,60 +67,60 @@ class AnayasaNormDenetimiSearchRequest(BaseModel):
|
|||||||
keywords_all: Optional[List[str]] = Field(default_factory=list, description="Keywords for AND logic (KelimeAra[]).")
|
keywords_all: Optional[List[str]] = Field(default_factory=list, description="Keywords for AND logic (KelimeAra[]).")
|
||||||
keywords_any: Optional[List[str]] = Field(default_factory=list, description="Keywords for OR logic (HerhangiBirKelimeAra[]).")
|
keywords_any: Optional[List[str]] = Field(default_factory=list, description="Keywords for OR logic (HerhangiBirKelimeAra[]).")
|
||||||
keywords_exclude: Optional[List[str]] = Field(default_factory=list, description="Keywords to exclude (BulunmayanKelimeAra[]).")
|
keywords_exclude: Optional[List[str]] = Field(default_factory=list, description="Keywords to exclude (BulunmayanKelimeAra[]).")
|
||||||
period: Optional[AnayasaDonemEnum] = Field(default=AnayasaDonemEnum.TUMU, description="Constitutional period (Donemler_id).")
|
period: Optional[Literal["ALL", "1", "2"]] = Field(default="ALL", description="Constitutional period (Donemler_id).")
|
||||||
case_number_esas: Optional[str] = Field(None, description="Case registry number (EsasNo), e.g., '2023/123'.")
|
case_number_esas: str = Field("", description="Case registry number (EsasNo), e.g., '2023/123'.")
|
||||||
decision_number_karar: Optional[str] = Field(None, description="Decision number (KararNo), e.g., '2023/456'.")
|
decision_number_karar: str = Field("", description="Decision number (KararNo), e.g., '2023/456'.")
|
||||||
first_review_date_start: Optional[str] = Field(None, description="First review start date (IlkIncelemeTarihiIlk), format DD/MM/YYYY.")
|
first_review_date_start: str = Field("", description="First review start date (IlkIncelemeTarihiIlk), format DD/MM/YYYY.")
|
||||||
first_review_date_end: Optional[str] = Field(None, description="First review end date (IlkIncelemeTarihiSon), format DD/MM/YYYY.")
|
first_review_date_end: str = Field("", description="First review end date (IlkIncelemeTarihiSon), format DD/MM/YYYY.")
|
||||||
decision_date_start: Optional[str] = Field(None, description="Decision start date (KararTarihiIlk), format DD/MM/YYYY.")
|
decision_date_start: str = Field("", description="Decision start date (KararTarihiIlk), format DD/MM/YYYY.")
|
||||||
decision_date_end: Optional[str] = Field(None, description="Decision end date (KararTarihiSon), format DD/MM/YYYY.")
|
decision_date_end: str = Field("", description="Decision end date (KararTarihiSon), format DD/MM/YYYY.")
|
||||||
application_type: Optional[AnayasaBasvuruTuruEnum] = Field(default=AnayasaBasvuruTuruEnum.TUMU, description="Type of application (BasvuruTurler_id).")
|
application_type: Optional[Literal["ALL", "1", "2", "3"]] = Field(default="ALL", description="Type of application (BasvuruTurler_id).")
|
||||||
applicant_general_name: Optional[str] = Field(None, description="General applicant name (BasvuranGeneller_id).")
|
applicant_general_name: str = Field("", description="General applicant name (BasvuranGeneller_id).")
|
||||||
applicant_specific_name: Optional[str] = Field(None, description="Specific applicant name (BasvuranOzeller_id).")
|
applicant_specific_name: str = Field("", description="Specific applicant name (BasvuranOzeller_id).")
|
||||||
official_gazette_date_start: Optional[str] = Field(None, description="Official Gazette start date (ResmiGazeteTarihiIlk), format DD/MM/YYYY.")
|
official_gazette_date_start: str = Field("", description="Official Gazette start date (ResmiGazeteTarihiIlk), format DD/MM/YYYY.")
|
||||||
official_gazette_date_end: Optional[str] = Field(None, description="Official Gazette end date (ResmiGazeteTarihiSon), format DD/MM/YYYY.")
|
official_gazette_date_end: str = Field("", description="Official Gazette end date (ResmiGazeteTarihiSon), format DD/MM/YYYY.")
|
||||||
official_gazette_number_start: Optional[str] = Field(None, description="Official Gazette starting number (ResmiGazeteSayisiIlk).")
|
official_gazette_number_start: str = Field("", description="Official Gazette starting number (ResmiGazeteSayisiIlk).")
|
||||||
official_gazette_number_end: Optional[str] = Field(None, description="Official Gazette ending number (ResmiGazeteSayisiSon).")
|
official_gazette_number_end: str = Field("", description="Official Gazette ending number (ResmiGazeteSayisiSon).")
|
||||||
has_press_release: Optional[AnayasaVarYokEnum] = Field(default=AnayasaVarYokEnum.TUMU, description="Press release available (BasinDuyurusu).")
|
has_press_release: Optional[Literal["ALL", "0", "1"]] = Field(default="ALL", description="Press release available (BasinDuyurusu).")
|
||||||
has_dissenting_opinion: Optional[AnayasaVarYokEnum] = Field(default=AnayasaVarYokEnum.TUMU, description="Dissenting opinion exists (KarsiOy).")
|
has_dissenting_opinion: Optional[Literal["ALL", "0", "1"]] = Field(default="ALL", description="Dissenting opinion exists (KarsiOy).")
|
||||||
has_different_reasoning: Optional[AnayasaVarYokEnum] = Field(default=AnayasaVarYokEnum.TUMU, description="Different reasoning exists (FarkliGerekce).")
|
has_different_reasoning: Optional[Literal["ALL", "0", "1"]] = Field(default="ALL", description="Different reasoning exists (FarkliGerekce).")
|
||||||
attending_members_names: Optional[List[str]] = Field(default_factory=list, description="List of attending members' exact names (Uyeler_id[]).")
|
attending_members_names: Optional[List[str]] = Field(default_factory=list, description="List of attending members' exact names (Uyeler_id[]).")
|
||||||
rapporteur_name: Optional[str] = Field(None, description="Rapporteur's exact name (Raportorler_id).")
|
rapporteur_name: str = Field("", description="Rapporteur's exact name (Raportorler_id).")
|
||||||
norm_type: Optional[AnayasaNormTuruEnum] = Field(default=AnayasaNormTuruEnum.TUMU, description="Type of the reviewed norm (NormunTurler_id).")
|
norm_type: Optional[Literal["ALL", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "0"]] = Field(default="ALL", description="Type of the reviewed norm (NormunTurler_id).")
|
||||||
norm_id_or_name: Optional[str] = Field(None, description="Number or name of the norm (NormunNumarasiAdlar_id).")
|
norm_id_or_name: str = Field("", description="Number or name of the norm (NormunNumarasiAdlar_id).")
|
||||||
norm_article: Optional[str] = Field(None, description="Article number of the norm (NormunMaddeNumarasi).")
|
norm_article: str = Field("", description="Article number of the norm (NormunMaddeNumarasi).")
|
||||||
review_outcomes: Optional[List[AnayasaIncelemeSonucuEnum]] = Field(default_factory=list, description="List of review types and outcomes (IncelemeTuruKararSonuclar_id[]).")
|
review_outcomes: Optional[List[Literal["1", "2", "3", "4", "5", "6", "7", "8", "12"]]] = Field(default_factory=list, description="List of review types and outcomes (IncelemeTuruKararSonuclar_id[]).")
|
||||||
reason_for_final_outcome: Optional[AnayasaSonucGerekcesiEnum] = Field(default=AnayasaSonucGerekcesiEnum.TUMU, description="Main reason for the decision outcome (KararSonucununGerekcesi).")
|
reason_for_final_outcome: Optional[Literal["ALL", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "29", "30"]] = Field(default="ALL", description="Main reason for the decision outcome (KararSonucununGerekcesi).")
|
||||||
basis_constitution_article_numbers: Optional[List[str]] = Field(default_factory=list, description="List of supporting Constitution article numbers (DayanakHukmu[]).")
|
basis_constitution_article_numbers: Optional[List[str]] = Field(default_factory=list, description="List of supporting Constitution article numbers (DayanakHukmu[]).")
|
||||||
results_per_page: Optional[int] = Field(10, description="Number of results per page. Options: 10, 20, 30, 40, 50.")
|
results_per_page: int = Field(10, ge=1, le=10, description="Results per page.")
|
||||||
page_to_fetch: Optional[int] = Field(1, ge=1, description="Page number to fetch for results list.")
|
page_to_fetch: int = Field(1, ge=1, description="Page number to fetch for results list.")
|
||||||
sort_by_criteria: Optional[str] = Field("KararTarihi", description="Sort criteria. Options: 'KararTarihi', 'YayinTarihi', 'Toplam' (keyword count).")
|
sort_by_criteria: str = Field("KararTarihi", description="Sort criteria. Options: 'KararTarihi', 'YayinTarihi', 'Toplam' (keyword count).")
|
||||||
|
|
||||||
class AnayasaReviewedNormInfo(BaseModel):
|
class AnayasaReviewedNormInfo(BaseModel):
|
||||||
"""Details of a norm reviewed within an AYM decision summary."""
|
"""Details of a norm reviewed within an AYM decision summary."""
|
||||||
norm_name_or_number: Optional[str] = None
|
norm_name_or_number: str = Field("", description="Norm name or number")
|
||||||
article_number: Optional[str] = None
|
article_number: str = Field("", description="Article number")
|
||||||
review_type_and_outcome: Optional[str] = None
|
review_type_and_outcome: str = Field("", description="Review type and outcome")
|
||||||
outcome_reason: Optional[str] = None
|
outcome_reason: str = Field("", description="Outcome reason")
|
||||||
basis_constitution_articles_cited: List[str] = Field(default_factory=list)
|
basis_constitution_articles_cited: List[str] = Field(default_factory=list)
|
||||||
postponement_period: Optional[str] = None
|
postponement_period: str = Field("", description="Postponement period")
|
||||||
|
|
||||||
class AnayasaDecisionSummary(BaseModel):
|
class AnayasaDecisionSummary(BaseModel):
|
||||||
"""Model for a single Anayasa Mahkemesi (Norm Denetimi) decision summary from search results."""
|
"""Model for a single Anayasa Mahkemesi (Norm Denetimi) decision summary from search results."""
|
||||||
decision_reference_no: Optional[str] = None
|
decision_reference_no: str = Field("", description="Decision reference number")
|
||||||
decision_page_url: Optional[HttpUrl] = None
|
decision_page_url: str = Field("", description="Decision page URL")
|
||||||
keywords_found_count: Optional[int] = None
|
keywords_found_count: Optional[int] = Field(0, description="Keywords found count")
|
||||||
application_type_summary: Optional[str] = None
|
application_type_summary: str = Field("", description="Application type summary")
|
||||||
applicant_summary: Optional[str] = None
|
applicant_summary: str = Field("", description="Applicant summary")
|
||||||
decision_outcome_summary: Optional[str] = None
|
decision_outcome_summary: str = Field("", description="Decision outcome summary")
|
||||||
decision_date_summary: Optional[str] = None
|
decision_date_summary: str = Field("", description="Decision date summary")
|
||||||
reviewed_norms: List[AnayasaReviewedNormInfo] = Field(default_factory=list)
|
reviewed_norms: List[AnayasaReviewedNormInfo] = Field(default_factory=list)
|
||||||
|
|
||||||
class AnayasaSearchResult(BaseModel):
|
class AnayasaSearchResult(BaseModel):
|
||||||
"""Model for the overall search result for Anayasa Mahkemesi Norm Denetimi decisions."""
|
"""Model for the overall search result for Anayasa Mahkemesi Norm Denetimi decisions."""
|
||||||
decisions: List[AnayasaDecisionSummary]
|
decisions: List[AnayasaDecisionSummary]
|
||||||
total_records_found: Optional[int] = None
|
total_records_found: int = Field(0, description="Total records found")
|
||||||
retrieved_page_number: Optional[int] = None
|
retrieved_page_number: int = Field(1, description="Retrieved page number")
|
||||||
|
|
||||||
class AnayasaDocumentMarkdown(BaseModel):
|
class AnayasaDocumentMarkdown(BaseModel):
|
||||||
"""
|
"""
|
||||||
@@ -150,10 +128,10 @@ class AnayasaDocumentMarkdown(BaseModel):
|
|||||||
and pagination information.
|
and pagination information.
|
||||||
"""
|
"""
|
||||||
source_url: HttpUrl
|
source_url: HttpUrl
|
||||||
decision_reference_no_from_page: Optional[str] = Field(None, description="E.K. No parsed from the document page.")
|
decision_reference_no_from_page: str = Field("", description="E.K. No parsed from the document page.")
|
||||||
decision_date_from_page: Optional[str] = Field(None, description="Decision date parsed from the document page.")
|
decision_date_from_page: str = Field("", description="Decision date parsed from the document page.")
|
||||||
official_gazette_info_from_page: Optional[str] = Field(None, description="Official Gazette info parsed from the document page.")
|
official_gazette_info_from_page: str = Field("", description="Official Gazette info parsed from the document page.")
|
||||||
markdown_chunk: Optional[str] = Field(None, description="A 5,000 character chunk of the Markdown content.") # Corrected chunk size
|
markdown_chunk: str = Field("", description="A 5,000 character chunk of the Markdown content.") # Corrected chunk size
|
||||||
current_page: int = Field(description="The current page number of the markdown chunk (1-indexed).")
|
current_page: int = Field(description="The current page number of the markdown chunk (1-indexed).")
|
||||||
total_pages: int = Field(description="Total number of pages for the full markdown content.")
|
total_pages: int = Field(description="Total number of pages for the full markdown content.")
|
||||||
is_paginated: bool = Field(description="True if the full markdown content is split into multiple pages.")
|
is_paginated: bool = Field(description="True if the full markdown content is split into multiple pages.")
|
||||||
@@ -168,27 +146,27 @@ class AnayasaBireyselReportSearchRequest(BaseModel):
|
|||||||
|
|
||||||
class AnayasaBireyselReportDecisionDetail(BaseModel):
|
class AnayasaBireyselReportDecisionDetail(BaseModel):
|
||||||
"""Details of a specific right/claim within a Bireysel Başvuru decision summary in a report."""
|
"""Details of a specific right/claim within a Bireysel Başvuru decision summary in a report."""
|
||||||
hak: Optional[str] = Field(None, description="İhlal edildiği iddia edilen hak (örneğin, Mülkiyet hakkı).")
|
hak: str = Field("", description="İhlal edildiği iddia edilen hak (örneğin, Mülkiyet hakkı).")
|
||||||
mudahale_iddiasi: Optional[str] = Field(None, description="İhlale neden olan müdahale iddiası.")
|
mudahale_iddiasi: str = Field("", description="İhlale neden olan müdahale iddiası.")
|
||||||
sonuc: Optional[str] = Field(None, description="İnceleme sonucu (örneğin, İhlal, Düşme).")
|
sonuc: str = Field("", description="İnceleme sonucu (örneğin, İhlal, Düşme).")
|
||||||
giderim: Optional[str] = Field(None, description="Kararlaştırılan giderim (örneğin, Yeniden yargılama).")
|
giderim: str = Field("", description="Kararlaştırılan giderim (örneğin, Yeniden yargılama).")
|
||||||
|
|
||||||
class AnayasaBireyselReportDecisionSummary(BaseModel):
|
class AnayasaBireyselReportDecisionSummary(BaseModel):
|
||||||
"""Model for a single Anayasa Mahkemesi (Bireysel Başvuru) decision summary from a 'Karar Arama Raporu'."""
|
"""Model for a single Anayasa Mahkemesi (Bireysel Başvuru) decision summary from a 'Karar Arama Raporu'."""
|
||||||
title: Optional[str] = Field(None, description="Başvurunun başlığı (e.g., 'HASAN DURMUŞ Başvurusuna İlişkin Karar').")
|
title: str = Field("", description="Başvurunun başlığı (e.g., 'HASAN DURMUŞ Başvurusuna İlişkin Karar').")
|
||||||
decision_reference_no: Optional[str] = Field(None, description="Başvuru Numarası (e.g., '2019/19126').")
|
decision_reference_no: str = Field("", description="Başvuru Numarası (e.g., '2019/19126').")
|
||||||
decision_page_url: Optional[HttpUrl] = Field(None, description="URL to the full decision page.")
|
decision_page_url: str = Field("", description="URL to the full decision page.")
|
||||||
decision_type_summary: Optional[str] = Field(None, description="Karar Türü (Başvuru Sonucu) (e.g., 'Esas (İhlal)').")
|
decision_type_summary: str = Field("", description="Karar Türü (Başvuru Sonucu) (e.g., 'Esas (İhlal)').")
|
||||||
decision_making_body: Optional[str] = Field(None, description="Kararı Veren Birim (e.g., 'Genel Kurul', 'Birinci Bölüm').")
|
decision_making_body: str = Field("", description="Kararı Veren Birim (e.g., 'Genel Kurul', 'Birinci Bölüm').")
|
||||||
application_date_summary: Optional[str] = Field(None, description="Başvuru Tarihi (DD/MM/YYYY).")
|
application_date_summary: str = Field("", description="Başvuru Tarihi (DD/MM/YYYY).")
|
||||||
decision_date_summary: Optional[str] = Field(None, description="Karar Tarihi (DD/MM/YYYY).")
|
decision_date_summary: str = Field("", description="Karar Tarihi (DD/MM/YYYY).")
|
||||||
application_subject_summary: Optional[str] = Field(None, description="Başvuru konusunun özeti.")
|
application_subject_summary: str = Field("", description="Başvuru konusunun özeti.")
|
||||||
details: List[AnayasaBireyselReportDecisionDetail] = Field(default_factory=list, description="İncelenen haklar ve sonuçlarına ilişkin detaylar.")
|
details: List[AnayasaBireyselReportDecisionDetail] = Field(default_factory=list, description="İncelenen haklar ve sonuçlarına ilişkin detaylar.")
|
||||||
|
|
||||||
class AnayasaBireyselReportSearchResult(BaseModel):
|
class AnayasaBireyselReportSearchResult(BaseModel):
|
||||||
"""Model for the overall search result for Anayasa Mahkemesi 'Karar Arama Raporu'."""
|
"""Model for the overall search result for Anayasa Mahkemesi 'Karar Arama Raporu'."""
|
||||||
decisions: List[AnayasaBireyselReportDecisionSummary]
|
decisions: List[AnayasaBireyselReportDecisionSummary]
|
||||||
total_records_found: Optional[int] = Field(None, description="Raporda bulunan toplam karar sayısı.")
|
total_records_found: int = Field(0, description="Raporda bulunan toplam karar sayısı.")
|
||||||
retrieved_page_number: int = Field(description="Alınan rapor sayfa numarası.")
|
retrieved_page_number: int = Field(description="Alınan rapor sayfa numarası.")
|
||||||
|
|
||||||
|
|
||||||
@@ -209,4 +187,44 @@ class AnayasaBireyselBasvuruDocumentMarkdown(BaseModel):
|
|||||||
total_pages: int = Field(description="Total number of pages for the full markdown content.")
|
total_pages: int = Field(description="Total number of pages for the full markdown content.")
|
||||||
is_paginated: bool = Field(description="True if the full markdown content is split into multiple pages.")
|
is_paginated: bool = Field(description="True if the full markdown content is split into multiple pages.")
|
||||||
|
|
||||||
# --- End Models for Bireysel Başvuru ---
|
# --- End Models for Bireysel Başvuru ---
|
||||||
|
|
||||||
|
# --- Unified Models ---
|
||||||
|
class AnayasaUnifiedSearchRequest(BaseModel):
|
||||||
|
"""Unified search request for both Norm Denetimi and Bireysel Başvuru."""
|
||||||
|
decision_type: Literal["norm_denetimi", "bireysel_basvuru"] = Field(..., description="Decision type: norm_denetimi or bireysel_basvuru")
|
||||||
|
|
||||||
|
# Common parameters
|
||||||
|
keywords: List[str] = Field(default_factory=list, description="Keywords to search for")
|
||||||
|
page_to_fetch: int = Field(1, ge=1, le=100, description="Page number to fetch (1-100)")
|
||||||
|
results_per_page: int = Field(10, ge=1, le=100, description="Results per page (1-100)")
|
||||||
|
|
||||||
|
# Norm Denetimi specific parameters (ignored for bireysel_basvuru)
|
||||||
|
keywords_all: List[str] = Field(default_factory=list, description="All keywords must be present (norm_denetimi only)")
|
||||||
|
keywords_any: List[str] = Field(default_factory=list, description="Any of these keywords (norm_denetimi only)")
|
||||||
|
decision_type_norm: Literal["ALL", "1", "2", "3"] = Field("ALL", description="Decision type for norm denetimi")
|
||||||
|
application_date_start: str = Field("", description="Application start date (norm_denetimi only)")
|
||||||
|
application_date_end: str = Field("", description="Application end date (norm_denetimi only)")
|
||||||
|
|
||||||
|
# Bireysel Başvuru specific parameters (ignored for norm_denetimi)
|
||||||
|
decision_start_date: str = Field("", description="Decision start date (bireysel_basvuru only)")
|
||||||
|
decision_end_date: str = Field("", description="Decision end date (bireysel_basvuru only)")
|
||||||
|
norm_type: Literal["ALL", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "0"] = Field("ALL", description="Norm type (bireysel_basvuru only)")
|
||||||
|
subject_category: str = Field("", description="Subject category (bireysel_basvuru only)")
|
||||||
|
|
||||||
|
class AnayasaUnifiedSearchResult(BaseModel):
|
||||||
|
"""Unified search result containing decisions from either system."""
|
||||||
|
decision_type: Literal["norm_denetimi", "bireysel_basvuru"] = Field(..., description="Type of decisions returned")
|
||||||
|
decisions: List[Dict[str, Any]] = Field(default_factory=list, description="Decision list (structure varies by type)")
|
||||||
|
total_records_found: int = Field(0, description="Total number of records found")
|
||||||
|
retrieved_page_number: int = Field(1, description="Page number that was retrieved")
|
||||||
|
|
||||||
|
class AnayasaUnifiedDocumentMarkdown(BaseModel):
|
||||||
|
"""Unified document model for both Norm Denetimi and Bireysel Başvuru."""
|
||||||
|
decision_type: Literal["norm_denetimi", "bireysel_basvuru"] = Field(..., description="Type of document")
|
||||||
|
source_url: HttpUrl = Field(..., description="Source URL of the document")
|
||||||
|
document_data: Dict[str, Any] = Field(default_factory=dict, description="Document content and metadata")
|
||||||
|
markdown_chunk: Optional[str] = Field(None, description="Markdown content chunk")
|
||||||
|
current_page: int = Field(1, description="Current page number")
|
||||||
|
total_pages: int = Field(1, description="Total number of pages")
|
||||||
|
is_paginated: bool = Field(False, description="Whether document is paginated")
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
# anayasa_mcp_module/unified_client.py
|
||||||
|
# Unified client for both Norm Denetimi and Bireysel Başvuru
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from .models import (
|
||||||
|
AnayasaUnifiedSearchRequest,
|
||||||
|
AnayasaUnifiedSearchResult,
|
||||||
|
AnayasaUnifiedDocumentMarkdown,
|
||||||
|
# Removed AnayasaDecisionTypeEnum - now using string literals
|
||||||
|
AnayasaNormDenetimiSearchRequest,
|
||||||
|
AnayasaBireyselReportSearchRequest
|
||||||
|
)
|
||||||
|
from .client import AnayasaMahkemesiApiClient
|
||||||
|
from .bireysel_client import AnayasaBireyselBasvuruApiClient
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class AnayasaUnifiedClient:
|
||||||
|
"""Unified client that handles both Norm Denetimi and Bireysel Başvuru searches."""
|
||||||
|
|
||||||
|
def __init__(self, request_timeout: float = 60.0):
|
||||||
|
self.norm_client = AnayasaMahkemesiApiClient(request_timeout)
|
||||||
|
self.bireysel_client = AnayasaBireyselBasvuruApiClient(request_timeout)
|
||||||
|
|
||||||
|
async def search_unified(self, params: AnayasaUnifiedSearchRequest) -> AnayasaUnifiedSearchResult:
|
||||||
|
"""Unified search that routes to appropriate client based on decision_type."""
|
||||||
|
|
||||||
|
if params.decision_type == "norm_denetimi":
|
||||||
|
# Convert to norm denetimi request
|
||||||
|
norm_params = AnayasaNormDenetimiSearchRequest(
|
||||||
|
keywords_all=params.keywords_all or params.keywords,
|
||||||
|
keywords_any=params.keywords_any,
|
||||||
|
application_type=params.decision_type_norm,
|
||||||
|
page_to_fetch=params.page_to_fetch,
|
||||||
|
results_per_page=params.results_per_page
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await self.norm_client.search_norm_denetimi_decisions(norm_params)
|
||||||
|
|
||||||
|
# Convert to unified format
|
||||||
|
decisions_list = [decision.model_dump() for decision in result.decisions]
|
||||||
|
|
||||||
|
return AnayasaUnifiedSearchResult(
|
||||||
|
decision_type="norm_denetimi",
|
||||||
|
decisions=decisions_list,
|
||||||
|
total_records_found=result.total_records_found,
|
||||||
|
retrieved_page_number=result.retrieved_page_number
|
||||||
|
)
|
||||||
|
|
||||||
|
elif params.decision_type == "bireysel_basvuru":
|
||||||
|
# Convert to bireysel başvuru request
|
||||||
|
bireysel_params = AnayasaBireyselReportSearchRequest(
|
||||||
|
keywords=params.keywords,
|
||||||
|
decision_start_date=params.decision_start_date,
|
||||||
|
decision_end_date=params.decision_end_date,
|
||||||
|
norm_type=params.norm_type,
|
||||||
|
subject_category=params.subject_category,
|
||||||
|
page_to_fetch=params.page_to_fetch,
|
||||||
|
results_per_page=params.results_per_page
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await self.bireysel_client.search_bireysel_basvuru_report(bireysel_params)
|
||||||
|
|
||||||
|
# Convert to unified format
|
||||||
|
decisions_list = [decision.model_dump() for decision in result.decisions]
|
||||||
|
|
||||||
|
return AnayasaUnifiedSearchResult(
|
||||||
|
decision_type="bireysel_basvuru",
|
||||||
|
decisions=decisions_list,
|
||||||
|
total_records_found=result.total_records_found,
|
||||||
|
retrieved_page_number=result.retrieved_page_number
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported decision type: {params.decision_type}")
|
||||||
|
|
||||||
|
async def get_document_unified(self, document_url: str, page_number: int = 1) -> AnayasaUnifiedDocumentMarkdown:
|
||||||
|
"""Unified document retrieval that auto-detects the appropriate client."""
|
||||||
|
|
||||||
|
# Auto-detect decision type based on URL
|
||||||
|
parsed_url = urlparse(document_url)
|
||||||
|
|
||||||
|
if "normkararlarbilgibankasi" in parsed_url.netloc or "/ND/" in document_url:
|
||||||
|
# Norm Denetimi document
|
||||||
|
result = await self.norm_client.get_decision_document_as_markdown(document_url, page_number)
|
||||||
|
|
||||||
|
return AnayasaUnifiedDocumentMarkdown(
|
||||||
|
decision_type="norm_denetimi",
|
||||||
|
source_url=result.source_url,
|
||||||
|
document_data=result.model_dump(),
|
||||||
|
markdown_chunk=result.markdown_chunk,
|
||||||
|
current_page=result.current_page,
|
||||||
|
total_pages=result.total_pages,
|
||||||
|
is_paginated=result.is_paginated
|
||||||
|
)
|
||||||
|
|
||||||
|
elif "kararlarbilgibankasi" in parsed_url.netloc or "/BB/" in document_url:
|
||||||
|
# Bireysel Başvuru document
|
||||||
|
result = await self.bireysel_client.get_decision_document_as_markdown(document_url, page_number)
|
||||||
|
|
||||||
|
return AnayasaUnifiedDocumentMarkdown(
|
||||||
|
decision_type="bireysel_basvuru",
|
||||||
|
source_url=result.source_url,
|
||||||
|
document_data=result.model_dump(),
|
||||||
|
markdown_chunk=result.markdown_chunk,
|
||||||
|
current_page=result.current_page,
|
||||||
|
total_pages=result.total_pages,
|
||||||
|
is_paginated=result.is_paginated
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Cannot determine document type from URL: {document_url}")
|
||||||
|
|
||||||
|
async def close_client_session(self):
|
||||||
|
"""Close both client sessions."""
|
||||||
|
if hasattr(self.norm_client, 'close_client_session'):
|
||||||
|
await self.norm_client.close_client_session()
|
||||||
|
if hasattr(self.bireysel_client, 'close_client_session'):
|
||||||
|
await self.bireysel_client.close_client_session()
|
||||||
+77
-189
@@ -19,9 +19,10 @@ from fastapi.exception_handlers import http_exception_handler
|
|||||||
from starlette.middleware import Middleware
|
from starlette.middleware import Middleware
|
||||||
from starlette.middleware.cors import CORSMiddleware
|
from starlette.middleware.cors import CORSMiddleware
|
||||||
from starlette.responses import Response
|
from starlette.responses import Response
|
||||||
|
from starlette.requests import Request as StarletteRequest
|
||||||
|
|
||||||
# Import the fully configured MCP app with all tools
|
# Import the MCP app creator function
|
||||||
from mcp_server_main import app as mcp_server
|
from mcp_server_main import create_app
|
||||||
|
|
||||||
# Import Stripe webhook router
|
# Import Stripe webhook router
|
||||||
from stripe_webhook import router as stripe_router
|
from stripe_webhook import router as stripe_router
|
||||||
@@ -36,23 +37,69 @@ BASE_URL = os.getenv("BASE_URL", "https://yargimcp.com")
|
|||||||
# Setup logging
|
# Setup logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Configure CORS middleware
|
# Configure CORS and Auth middleware
|
||||||
cors_origins = os.getenv("ALLOWED_ORIGINS", "*").split(",")
|
cors_origins = os.getenv("ALLOWED_ORIGINS", "*").split(",")
|
||||||
|
|
||||||
|
# Import FastMCP Bearer Auth Provider
|
||||||
|
from fastmcp.server.auth import BearerAuthProvider
|
||||||
|
from fastmcp.server.auth.providers.bearer import RSAKeyPair
|
||||||
|
|
||||||
|
# Clerk JWT configuration for Bearer token validation
|
||||||
|
CLERK_SECRET_KEY = os.getenv("CLERK_SECRET_KEY")
|
||||||
|
CLERK_ISSUER = os.getenv("CLERK_ISSUER", "https://accounts.yargimcp.com")
|
||||||
|
CLERK_PUBLISHABLE_KEY = os.getenv("CLERK_PUBLISHABLE_KEY")
|
||||||
|
|
||||||
|
# Configure Bearer token authentication
|
||||||
|
bearer_auth = None
|
||||||
|
if CLERK_SECRET_KEY and CLERK_ISSUER:
|
||||||
|
# Production: Use Clerk JWKS endpoint for token validation
|
||||||
|
bearer_auth = BearerAuthProvider(
|
||||||
|
jwks_uri=f"{CLERK_ISSUER}/.well-known/jwks.json",
|
||||||
|
issuer=CLERK_ISSUER,
|
||||||
|
algorithm="RS256",
|
||||||
|
audience=None, # Disable audience validation - Clerk uses different audience format
|
||||||
|
required_scopes=[] # Disable scope validation - Clerk JWT has ['read', 'search']
|
||||||
|
)
|
||||||
|
logger.info(f"Bearer auth configured with Clerk JWKS: {CLERK_ISSUER}/.well-known/jwks.json")
|
||||||
|
else:
|
||||||
|
# Development: Generate RSA key pair for testing
|
||||||
|
logger.warning("No Clerk credentials found - using development RSA key pair")
|
||||||
|
dev_key_pair = RSAKeyPair.generate()
|
||||||
|
bearer_auth = BearerAuthProvider(
|
||||||
|
public_key=dev_key_pair.public_key,
|
||||||
|
issuer="https://dev.yargimcp.com",
|
||||||
|
audience="dev-mcp-server",
|
||||||
|
required_scopes=["yargi.read"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generate a test token for development
|
||||||
|
dev_token = dev_key_pair.create_token(
|
||||||
|
subject="dev-user",
|
||||||
|
issuer="https://dev.yargimcp.com",
|
||||||
|
audience="dev-mcp-server",
|
||||||
|
scopes=["yargi.read", "yargi.search"],
|
||||||
|
expires_in_seconds=3600 * 24 # 24 hours for development
|
||||||
|
)
|
||||||
|
logger.info(f"Development Bearer token: {dev_token}")
|
||||||
|
|
||||||
custom_middleware = [
|
custom_middleware = [
|
||||||
Middleware(
|
Middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=cors_origins,
|
allow_origins=cors_origins,
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["GET", "POST", "OPTIONS"],
|
allow_methods=["GET", "POST", "OPTIONS", "DELETE"],
|
||||||
allow_headers=["Content-Type", "Authorization", "X-Request-ID"],
|
allow_headers=["Content-Type", "Authorization", "X-Request-ID", "X-Session-ID"],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Create MCP Starlette sub-application (without auth wrapper)
|
# Create MCP app with Bearer authentication
|
||||||
mcp_app = mcp_server.http_app(
|
mcp_server = create_app(auth=bearer_auth)
|
||||||
path="/",
|
|
||||||
middleware=custom_middleware
|
# Add Starlette middleware to FastAPI (not MCP)
|
||||||
)
|
# MCP already has Bearer auth, no need for additional middleware on MCP level
|
||||||
|
|
||||||
|
# Create MCP Starlette sub-application with root path - mount will add /mcp prefix
|
||||||
|
mcp_app = mcp_server.http_app(path="/")
|
||||||
|
|
||||||
# Configure JSON encoder for proper Turkish character support
|
# Configure JSON encoder for proper Turkish character support
|
||||||
import json
|
import json
|
||||||
@@ -74,13 +121,12 @@ class UTF8JSONResponse(JSONResponse):
|
|||||||
separators=(",", ":"),
|
separators=(",", ":"),
|
||||||
).encode("utf-8")
|
).encode("utf-8")
|
||||||
|
|
||||||
# Create FastAPI wrapper application with MCP lifespan
|
# Create FastAPI wrapper application
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Yargı MCP Server",
|
title="Yargı MCP Server",
|
||||||
description="MCP server for Turkish legal databases with OAuth authentication",
|
description="MCP server for Turkish legal databases with OAuth authentication",
|
||||||
version="0.1.0",
|
version="0.1.0",
|
||||||
middleware=custom_middleware,
|
middleware=custom_middleware,
|
||||||
lifespan=mcp_app.lifespan, # MCP app lifespan
|
|
||||||
default_response_class=UTF8JSONResponse # Use UTF-8 JSON encoder
|
default_response_class=UTF8JSONResponse # Use UTF-8 JSON encoder
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -107,141 +153,7 @@ async def custom_401_handler(request: Request, exc: HTTPException):
|
|||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
# Mount MCP app as sub-application at /mcp-server to avoid path conflicts
|
# FastAPI health check endpoint - BEFORE mounting MCP app
|
||||||
app.mount("/mcp-server", mcp_app)
|
|
||||||
|
|
||||||
# Add custom route to handle /mcp requests and forward to mounted app
|
|
||||||
@app.api_route("/mcp", methods=["POST", "DELETE", "OPTIONS"])
|
|
||||||
@app.api_route("/mcp/", methods=["POST", "DELETE", "OPTIONS"])
|
|
||||||
async def mcp_protocol_handler(request: Request):
|
|
||||||
"""Handle MCP protocol requests by forwarding to mounted app"""
|
|
||||||
|
|
||||||
# Handle DELETE requests for session termination
|
|
||||||
if request.method == "DELETE":
|
|
||||||
logger.info("DELETE request received for session termination")
|
|
||||||
# For session termination, we just return 200 OK
|
|
||||||
# The actual session cleanup is handled by the underlying MCP transport
|
|
||||||
from starlette.responses import Response
|
|
||||||
return Response(
|
|
||||||
status_code=200,
|
|
||||||
content="Session terminated successfully"
|
|
||||||
)
|
|
||||||
|
|
||||||
# REQUIRED: Validate Bearer JWT tokens for all MCP requests
|
|
||||||
auth_header = request.headers.get("Authorization")
|
|
||||||
if not auth_header or not auth_header.startswith("Bearer "):
|
|
||||||
logger.error("Missing or invalid Authorization header")
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=401,
|
|
||||||
detail="Missing or invalid Authorization header. Bearer token required."
|
|
||||||
)
|
|
||||||
|
|
||||||
token = auth_header.split(" ")[1]
|
|
||||||
try:
|
|
||||||
# Check if this is a mock token for development/testing
|
|
||||||
if token.startswith("mock_clerk_jwt_"):
|
|
||||||
logger.info(f"Using mock JWT token for development: {token[:30]}...")
|
|
||||||
# For mock tokens, we'll allow access with a mock user
|
|
||||||
request.state.user_id = "mock_user_dev"
|
|
||||||
request.state.session_id = "mock_session_dev"
|
|
||||||
request.state.token_scopes = ["read", "search"]
|
|
||||||
logger.info("Mock JWT token accepted for development")
|
|
||||||
elif token.startswith("eyJ"):
|
|
||||||
# This looks like a real JWT token (starts with eyJ which is base64 encoded '{"')
|
|
||||||
logger.info(f"Processing real JWT token: {token[:30]}...")
|
|
||||||
# Validate real Clerk JWT token
|
|
||||||
from clerk_backend_api import Clerk, models
|
|
||||||
import jwt
|
|
||||||
|
|
||||||
# Decode JWT token and extract user info
|
|
||||||
try:
|
|
||||||
decoded_token = jwt.decode(token, options={"verify_signature": False})
|
|
||||||
user_id = decoded_token.get("user_id") or decoded_token.get("sub")
|
|
||||||
user_email = decoded_token.get("email")
|
|
||||||
token_scopes = decoded_token.get("scopes", ["read", "search"])
|
|
||||||
session_id = decoded_token.get("sid", "jwt_session")
|
|
||||||
|
|
||||||
logger.info(f"JWT token claims - user_id: {user_id}, email: {user_email}, scopes: {token_scopes}")
|
|
||||||
|
|
||||||
if user_id and user_email:
|
|
||||||
# JWT token is signed by Clerk and contains valid user info
|
|
||||||
request.state.user_id = user_id
|
|
||||||
request.state.user_email = user_email
|
|
||||||
request.state.session_id = session_id
|
|
||||||
request.state.token_scopes = token_scopes
|
|
||||||
logger.info(f"Real JWT token accepted for user: {user_id}")
|
|
||||||
else:
|
|
||||||
logger.error(f"Missing required fields in JWT token - user_id: {bool(user_id)}, email: {bool(user_email)}")
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=401,
|
|
||||||
detail="Invalid token - missing user_id or email in claims"
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"JWT token decoding failed: {e}")
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=401,
|
|
||||||
detail="Invalid JWT token format"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Invalid token format - doesn't start with expected patterns
|
|
||||||
logger.error(f"Invalid token format: {token[:30]}...")
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=401,
|
|
||||||
detail="Invalid token format - must be a valid JWT token"
|
|
||||||
)
|
|
||||||
|
|
||||||
except HTTPException:
|
|
||||||
# Re-raise HTTPException as-is
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Bearer token validation failed: {str(e)}")
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=401,
|
|
||||||
detail=f"Token validation failed: {str(e)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Forward the request to the mounted MCP app
|
|
||||||
async def receive():
|
|
||||||
return await request.receive()
|
|
||||||
|
|
||||||
# Create new scope for the mounted app
|
|
||||||
scope = request.scope.copy()
|
|
||||||
scope["path"] = "/" # Root path for mounted app
|
|
||||||
scope["path_info"] = "/"
|
|
||||||
|
|
||||||
# Capture the response
|
|
||||||
response_parts = {"status": 200, "headers": [], "body": b""}
|
|
||||||
|
|
||||||
async def send(message):
|
|
||||||
if message["type"] == "http.response.start":
|
|
||||||
response_parts["status"] = message["status"]
|
|
||||||
response_parts["headers"] = message["headers"]
|
|
||||||
elif message["type"] == "http.response.body":
|
|
||||||
response_parts["body"] += message.get("body", b"")
|
|
||||||
|
|
||||||
# Call the mounted MCP app
|
|
||||||
await mcp_app(scope, receive, send)
|
|
||||||
|
|
||||||
# Return the response
|
|
||||||
from starlette.responses import Response
|
|
||||||
|
|
||||||
# Convert ASGI headers to dict
|
|
||||||
headers = {}
|
|
||||||
for name, value in response_parts["headers"]:
|
|
||||||
headers[name.decode()] = value.decode()
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
content=response_parts["body"],
|
|
||||||
status_code=response_parts["status"],
|
|
||||||
headers=headers
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# SSE transport deprecated - removed
|
|
||||||
|
|
||||||
|
|
||||||
# FastAPI health check endpoint
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
async def health_check():
|
async def health_check():
|
||||||
"""Health check endpoint for monitoring"""
|
"""Health check endpoint for monitoring"""
|
||||||
@@ -253,6 +165,22 @@ async def health_check():
|
|||||||
"auth_enabled": os.getenv("ENABLE_AUTH", "false").lower() == "true"
|
"auth_enabled": os.getenv("ENABLE_AUTH", "false").lower() == "true"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Add explicit redirect for /mcp to /mcp/ with method preservation
|
||||||
|
@app.api_route("/mcp", methods=["GET", "POST", "HEAD", "OPTIONS"])
|
||||||
|
async def redirect_to_slash(request: Request):
|
||||||
|
"""Redirect /mcp to /mcp/ preserving HTTP method with 308"""
|
||||||
|
from fastapi.responses import RedirectResponse
|
||||||
|
return RedirectResponse(url="/mcp/", status_code=308)
|
||||||
|
|
||||||
|
# Mount MCP app at /mcp/ with trailing slash
|
||||||
|
app.mount("/mcp/", mcp_app)
|
||||||
|
|
||||||
|
# Set the lifespan context after mounting
|
||||||
|
app.router.lifespan_context = mcp_app.lifespan
|
||||||
|
|
||||||
|
|
||||||
|
# SSE transport deprecated - removed
|
||||||
|
|
||||||
# FastAPI root endpoint
|
# FastAPI root endpoint
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
@@ -374,48 +302,8 @@ async def oauth_authorization_server_root():
|
|||||||
"resource_documentation": f"{BASE_URL}/mcp"
|
"resource_documentation": f"{BASE_URL}/mcp"
|
||||||
})
|
})
|
||||||
|
|
||||||
# MCP endpoint info for GET requests (ChatGPT compatibility)
|
# Note: GET /mcp is handled by the mounted MCP app itself
|
||||||
@app.get("/mcp")
|
# This prevents 405 Method Not Allowed errors on POST requests
|
||||||
async def mcp_info():
|
|
||||||
"""MCP endpoint information for discovery"""
|
|
||||||
return JSONResponse({
|
|
||||||
"mcp_server": True,
|
|
||||||
"name": "Yargı MCP Server",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "MCP server for Turkish legal databases",
|
|
||||||
"protocol": "mcp/1.0",
|
|
||||||
"transport": ["http"],
|
|
||||||
"authentication_required": True,
|
|
||||||
"authentication": {
|
|
||||||
"type": "oauth2",
|
|
||||||
"authorization_url": "https://yargimcp.com/sign-in?redirect_url=https://api.yargimcp.com/auth/mcp-callback",
|
|
||||||
"token_url": f"{BASE_URL}/auth/mcp-token",
|
|
||||||
"scopes": ["read", "search"],
|
|
||||||
"provider": "clerk"
|
|
||||||
},
|
|
||||||
"endpoints": {
|
|
||||||
"mcp_protocol": "/mcp",
|
|
||||||
"discovery": "/mcp/discovery",
|
|
||||||
"well_known": "/.well-known/mcp",
|
|
||||||
"health": "/health",
|
|
||||||
"oauth_login": "/auth/login"
|
|
||||||
},
|
|
||||||
"capabilities": {
|
|
||||||
"tools": True,
|
|
||||||
"resources": True,
|
|
||||||
"prompts": False
|
|
||||||
},
|
|
||||||
"tools_count": len(mcp_server._tool_manager._tools),
|
|
||||||
"usage": {
|
|
||||||
"note": "This is an MCP server. Use POST to /mcp/ with proper MCP protocol headers.",
|
|
||||||
"headers_required": [
|
|
||||||
"Content-Type: application/json",
|
|
||||||
"Accept: application/json",
|
|
||||||
"Authorization: Bearer <token>",
|
|
||||||
"X-Session-ID: <session-id>"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# OAuth 2.0 Protected Resource Metadata (RFC 9728) - MCP Spec Required
|
# OAuth 2.0 Protected Resource Metadata (RFC 9728) - MCP Spec Required
|
||||||
@app.get("/.well-known/oauth-protected-resource")
|
@app.get("/.well-known/oauth-protected-resource")
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# bddk_mcp_module/__init__.py
|
||||||
|
|
||||||
|
from .client import BddkApiClient
|
||||||
|
from .models import (
|
||||||
|
BddkSearchRequest,
|
||||||
|
BddkDecisionSummary,
|
||||||
|
BddkSearchResult,
|
||||||
|
BddkDocumentMarkdown
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"BddkApiClient",
|
||||||
|
"BddkSearchRequest",
|
||||||
|
"BddkDecisionSummary",
|
||||||
|
"BddkSearchResult",
|
||||||
|
"BddkDocumentMarkdown"
|
||||||
|
]
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
# bddk_mcp_module/client.py
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from typing import List, Optional, Dict, Any
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import io
|
||||||
|
import math
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
from markitdown import MarkItDown
|
||||||
|
|
||||||
|
from .models import (
|
||||||
|
BddkSearchRequest,
|
||||||
|
BddkDecisionSummary,
|
||||||
|
BddkSearchResult,
|
||||||
|
BddkDocumentMarkdown
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
if not logger.hasHandlers():
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
|
||||||
|
class BddkApiClient:
|
||||||
|
"""
|
||||||
|
API client for searching and retrieving BDDK (Banking Regulation Authority) decisions
|
||||||
|
using Tavily Search API for discovery and direct HTTP requests for content retrieval.
|
||||||
|
"""
|
||||||
|
|
||||||
|
TAVILY_API_URL = "https://api.tavily.com/search"
|
||||||
|
BDDK_BASE_URL = "https://www.bddk.org.tr"
|
||||||
|
DOCUMENT_URL_TEMPLATE = "https://www.bddk.org.tr/Mevzuat/DokumanGetir/{document_id}"
|
||||||
|
DOCUMENT_MARKDOWN_CHUNK_SIZE = 5000 # Character limit per page
|
||||||
|
|
||||||
|
def __init__(self, request_timeout: float = 60.0):
|
||||||
|
"""Initialize the BDDK API client."""
|
||||||
|
self.tavily_api_key = os.getenv("TAVILY_API_KEY")
|
||||||
|
if not self.tavily_api_key:
|
||||||
|
# Fallback to development token
|
||||||
|
self.tavily_api_key = "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"
|
||||||
|
logger.info("Using fallback Tavily API token (development token)")
|
||||||
|
else:
|
||||||
|
logger.info("Using Tavily API key from environment variable")
|
||||||
|
|
||||||
|
self.http_client = httpx.AsyncClient(
|
||||||
|
headers={
|
||||||
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
|
||||||
|
},
|
||||||
|
timeout=httpx.Timeout(request_timeout)
|
||||||
|
)
|
||||||
|
self.markitdown = MarkItDown()
|
||||||
|
|
||||||
|
async def close_client_session(self):
|
||||||
|
"""Close the HTTP client session."""
|
||||||
|
await self.http_client.aclose()
|
||||||
|
logger.info("BddkApiClient: HTTP client session closed.")
|
||||||
|
|
||||||
|
def _extract_document_id(self, url: str) -> Optional[str]:
|
||||||
|
"""Extract document ID from BDDK URL."""
|
||||||
|
# Primary pattern: https://www.bddk.org.tr/Mevzuat/DokumanGetir/310
|
||||||
|
match = re.search(r'/DokumanGetir/(\d+)', url)
|
||||||
|
if match:
|
||||||
|
return match.group(1)
|
||||||
|
|
||||||
|
# Alternative patterns for different BDDK URL formats
|
||||||
|
# Pattern: /Liste/55 -> use as document ID
|
||||||
|
match = re.search(r'/Liste/(\d+)', url)
|
||||||
|
if match:
|
||||||
|
return match.group(1)
|
||||||
|
|
||||||
|
# Pattern: /EkGetir/13?ekId=381 -> use ekId as document ID
|
||||||
|
match = re.search(r'ekId=(\d+)', url)
|
||||||
|
if match:
|
||||||
|
return match.group(1)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def search_decisions(
|
||||||
|
self,
|
||||||
|
request: BddkSearchRequest
|
||||||
|
) -> BddkSearchResult:
|
||||||
|
"""
|
||||||
|
Search for BDDK decisions using Tavily API.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: Search request parameters
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BddkSearchResult with matching decisions
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": f"Bearer {self.tavily_api_key}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Tavily API request - enhanced for BDDK decision documents
|
||||||
|
query = f"{request.keywords} \"Karar Sayısı\""
|
||||||
|
payload = {
|
||||||
|
"query": query,
|
||||||
|
"country": "turkey",
|
||||||
|
"include_domains": ["https://www.bddk.org.tr/Mevzuat/DokumanGetir"],
|
||||||
|
"max_results": request.pageSize,
|
||||||
|
"search_depth": "advanced"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Calculate offset for pagination
|
||||||
|
if request.page > 1:
|
||||||
|
# Tavily doesn't have direct pagination, so we'll need to handle this
|
||||||
|
# For now, we'll just return empty for pages > 1
|
||||||
|
logger.warning(f"Tavily API doesn't support pagination. Page {request.page} requested.")
|
||||||
|
|
||||||
|
response = await self.http_client.post(
|
||||||
|
self.TAVILY_API_URL,
|
||||||
|
json=payload,
|
||||||
|
headers=headers
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
# Log raw Tavily response for debugging
|
||||||
|
logger.info(f"Tavily returned {len(data.get('results', []))} results")
|
||||||
|
|
||||||
|
# Convert Tavily results to our format
|
||||||
|
decisions = []
|
||||||
|
for result in data.get("results", []):
|
||||||
|
# Extract document ID from URL
|
||||||
|
url = result.get("url", "")
|
||||||
|
logger.debug(f"Processing URL: {url}")
|
||||||
|
doc_id = self._extract_document_id(url)
|
||||||
|
if doc_id:
|
||||||
|
decision = BddkDecisionSummary(
|
||||||
|
title=result.get("title", "").replace("[PDF] ", "").strip(),
|
||||||
|
document_id=doc_id,
|
||||||
|
content=result.get("content", "")[:500] # Limit content length
|
||||||
|
)
|
||||||
|
decisions.append(decision)
|
||||||
|
logger.debug(f"Added decision: {decision.title} (ID: {doc_id})")
|
||||||
|
else:
|
||||||
|
logger.warning(f"Could not extract document ID from URL: {url}")
|
||||||
|
|
||||||
|
return BddkSearchResult(
|
||||||
|
decisions=decisions,
|
||||||
|
total_results=len(data.get("results", [])),
|
||||||
|
page=request.page,
|
||||||
|
pageSize=request.pageSize
|
||||||
|
)
|
||||||
|
|
||||||
|
except httpx.HTTPStatusError as e:
|
||||||
|
logger.error(f"HTTP error searching BDDK decisions: {e}")
|
||||||
|
if e.response.status_code == 401:
|
||||||
|
raise Exception("Tavily API authentication failed. Check API key.")
|
||||||
|
raise Exception(f"Failed to search BDDK decisions: {str(e)}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error searching BDDK decisions: {e}")
|
||||||
|
raise Exception(f"Failed to search BDDK decisions: {str(e)}")
|
||||||
|
|
||||||
|
async def get_document_markdown(
|
||||||
|
self,
|
||||||
|
document_id: str,
|
||||||
|
page_number: int = 1
|
||||||
|
) -> BddkDocumentMarkdown:
|
||||||
|
"""
|
||||||
|
Retrieve a BDDK document and convert it to Markdown format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
document_id: BDDK document ID (e.g., '310')
|
||||||
|
page_number: Page number for paginated content (1-indexed)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BddkDocumentMarkdown with paginated content
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Try different URL patterns for BDDK documents
|
||||||
|
potential_urls = [
|
||||||
|
f"https://www.bddk.org.tr/Mevzuat/DokumanGetir/{document_id}",
|
||||||
|
f"https://www.bddk.org.tr/Mevzuat/Liste/{document_id}",
|
||||||
|
f"https://www.bddk.org.tr/KurumHakkinda/EkGetir/13?ekId={document_id}",
|
||||||
|
f"https://www.bddk.org.tr/KurumHakkinda/EkGetir/5?ekId={document_id}"
|
||||||
|
]
|
||||||
|
|
||||||
|
document_url = None
|
||||||
|
response = None
|
||||||
|
|
||||||
|
# Try each URL pattern until one works
|
||||||
|
for url in potential_urls:
|
||||||
|
try:
|
||||||
|
logger.info(f"Trying BDDK document URL: {url}")
|
||||||
|
response = await self.http_client.get(
|
||||||
|
url,
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
document_url = url
|
||||||
|
break
|
||||||
|
except httpx.HTTPStatusError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not response or not document_url:
|
||||||
|
raise Exception(f"Could not find document with ID {document_id}")
|
||||||
|
|
||||||
|
logger.info(f"Successfully fetched BDDK document from: {document_url}")
|
||||||
|
|
||||||
|
# Determine content type
|
||||||
|
content_type = response.headers.get("content-type", "").lower()
|
||||||
|
|
||||||
|
# Convert to Markdown based on content type
|
||||||
|
if "pdf" in content_type:
|
||||||
|
# Handle PDF documents
|
||||||
|
pdf_stream = io.BytesIO(response.content)
|
||||||
|
result = self.markitdown.convert_stream(pdf_stream, file_extension=".pdf")
|
||||||
|
markdown_content = result.text_content
|
||||||
|
else:
|
||||||
|
# Handle HTML documents
|
||||||
|
html_stream = io.BytesIO(response.content)
|
||||||
|
result = self.markitdown.convert_stream(html_stream, file_extension=".html")
|
||||||
|
markdown_content = result.text_content
|
||||||
|
|
||||||
|
# Clean up the markdown content
|
||||||
|
markdown_content = markdown_content.strip()
|
||||||
|
|
||||||
|
# Calculate pagination
|
||||||
|
total_length = len(markdown_content)
|
||||||
|
total_pages = math.ceil(total_length / self.DOCUMENT_MARKDOWN_CHUNK_SIZE)
|
||||||
|
|
||||||
|
# Extract the requested page
|
||||||
|
start_idx = (page_number - 1) * self.DOCUMENT_MARKDOWN_CHUNK_SIZE
|
||||||
|
end_idx = start_idx + self.DOCUMENT_MARKDOWN_CHUNK_SIZE
|
||||||
|
page_content = markdown_content[start_idx:end_idx]
|
||||||
|
|
||||||
|
return BddkDocumentMarkdown(
|
||||||
|
document_id=document_id,
|
||||||
|
markdown_content=page_content,
|
||||||
|
page_number=page_number,
|
||||||
|
total_pages=total_pages
|
||||||
|
)
|
||||||
|
|
||||||
|
except httpx.HTTPStatusError as e:
|
||||||
|
logger.error(f"HTTP error fetching BDDK document {document_id}: {e}")
|
||||||
|
raise Exception(f"Failed to fetch BDDK document: {str(e)}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing BDDK document {document_id}: {e}")
|
||||||
|
raise Exception(f"Failed to process BDDK document: {str(e)}")
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# bddk_mcp_module/models.py
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
class BddkSearchRequest(BaseModel):
|
||||||
|
"""
|
||||||
|
Request model for searching BDDK decisions via Tavily API.
|
||||||
|
|
||||||
|
BDDK (Bankacılık Düzenleme ve Denetleme Kurumu) is Turkey's Banking
|
||||||
|
Regulation and Supervision Agency responsible for banking licenses,
|
||||||
|
electronic money institutions, and financial regulations.
|
||||||
|
"""
|
||||||
|
keywords: str = Field(..., description="Search keywords in Turkish")
|
||||||
|
page: int = Field(1, ge=1, description="Page number (1-indexed)")
|
||||||
|
pageSize: int = Field(10, ge=1, le=50, description="Results per page (1-50)")
|
||||||
|
|
||||||
|
class BddkDecisionSummary(BaseModel):
|
||||||
|
"""Summary of a BDDK decision from search results."""
|
||||||
|
title: str = Field(..., description="Decision title")
|
||||||
|
document_id: str = Field(..., description="BDDK document ID (e.g., '310')")
|
||||||
|
content: str = Field(..., description="Decision summary/excerpt")
|
||||||
|
|
||||||
|
class BddkSearchResult(BaseModel):
|
||||||
|
"""Response model for BDDK decision search results."""
|
||||||
|
decisions: List[BddkDecisionSummary] = Field(
|
||||||
|
default_factory=list,
|
||||||
|
description="List of matching BDDK decisions"
|
||||||
|
)
|
||||||
|
total_results: int = Field(0, description="Total number of results")
|
||||||
|
page: int = Field(1, description="Current page number")
|
||||||
|
pageSize: int = Field(10, description="Results per page")
|
||||||
|
|
||||||
|
class BddkDocumentMarkdown(BaseModel):
|
||||||
|
"""
|
||||||
|
BDDK decision document converted to Markdown format.
|
||||||
|
|
||||||
|
Supports paginated content for long documents (5000 chars per page).
|
||||||
|
"""
|
||||||
|
document_id: str = Field(..., description="BDDK document ID")
|
||||||
|
markdown_content: str = Field("", description="Document content in Markdown")
|
||||||
|
page_number: int = Field(1, description="Current page number")
|
||||||
|
total_pages: int = Field(1, description="Total number of pages")
|
||||||
@@ -12,6 +12,7 @@ from .models import (
|
|||||||
BedestenDocumentRequest, BedestenDocumentResponse,
|
BedestenDocumentRequest, BedestenDocumentResponse,
|
||||||
BedestenDocumentMarkdown, BedestenDocumentRequestData
|
BedestenDocumentMarkdown, BedestenDocumentRequestData
|
||||||
)
|
)
|
||||||
|
from .enums import get_full_birim_adi
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -49,10 +50,22 @@ class BedestenApiClient:
|
|||||||
"""
|
"""
|
||||||
logger.info(f"BedestenApiClient: Searching documents with phrase: {search_request.data.phrase}")
|
logger.info(f"BedestenApiClient: Searching documents with phrase: {search_request.data.phrase}")
|
||||||
|
|
||||||
|
# Map abbreviated birimAdi to full Turkish name before sending to API
|
||||||
|
original_birim_adi = search_request.data.birimAdi
|
||||||
|
mapped_birim_adi = get_full_birim_adi(original_birim_adi)
|
||||||
|
search_request.data.birimAdi = mapped_birim_adi
|
||||||
|
if original_birim_adi != "ALL":
|
||||||
|
logger.info(f"BedestenApiClient: Mapped birimAdi '{original_birim_adi}' to '{mapped_birim_adi}'")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Create request dict and remove birimAdi if empty
|
||||||
|
request_dict = search_request.model_dump()
|
||||||
|
if not request_dict["data"]["birimAdi"]: # Remove if empty string
|
||||||
|
del request_dict["data"]["birimAdi"]
|
||||||
|
|
||||||
response = await self.http_client.post(
|
response = await self.http_client.post(
|
||||||
self.SEARCH_ENDPOINT,
|
self.SEARCH_ENDPOINT,
|
||||||
json=search_request.model_dump()
|
json=request_dict
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
response_json = response.json()
|
response_json = response.json()
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# bedesten_mcp_module/enums.py
|
||||||
|
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
# Unified compressed enum for both Yargıtay and Danıştay chambers
|
||||||
|
BirimAdiEnum = Literal[
|
||||||
|
"ALL", # All chambers
|
||||||
|
|
||||||
|
# Yargıtay (Court of Cassation) - Civil Chambers
|
||||||
|
"H1", "H2", "H3", "H4", "H5", "H6", "H7", "H8", "H9", "H10",
|
||||||
|
"H11", "H12", "H13", "H14", "H15", "H16", "H17", "H18", "H19", "H20",
|
||||||
|
"H21", "H22", "H23",
|
||||||
|
|
||||||
|
# Yargıtay - Criminal Chambers
|
||||||
|
"C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9", "C10",
|
||||||
|
"C11", "C12", "C13", "C14", "C15", "C16", "C17", "C18", "C19", "C20",
|
||||||
|
"C21", "C22", "C23",
|
||||||
|
|
||||||
|
# Yargıtay - Councils and Assemblies
|
||||||
|
"HGK", # Hukuk Genel Kurulu
|
||||||
|
"CGK", # Ceza Genel Kurulu
|
||||||
|
"BGK", # Büyük Genel Kurulu
|
||||||
|
"HBK", # Hukuk Daireleri Başkanlar Kurulu
|
||||||
|
"CBK", # Ceza Daireleri Başkanlar Kurulu
|
||||||
|
|
||||||
|
# Danıştay (Council of State) - Chambers
|
||||||
|
"D1", "D2", "D3", "D4", "D5", "D6", "D7", "D8", "D9", "D10",
|
||||||
|
"D11", "D12", "D13", "D14", "D15", "D16", "D17",
|
||||||
|
|
||||||
|
# Danıştay - Councils and Boards
|
||||||
|
"DBGK", # Büyük Gen.Kur. (Grand General Assembly)
|
||||||
|
"IDDK", # İdare Dava Daireleri Kurulu
|
||||||
|
"VDDK", # Vergi Dava Daireleri Kurulu
|
||||||
|
"IBK", # İçtihatları Birleştirme Kurulu
|
||||||
|
"IIK", # İdari İşler Kurulu
|
||||||
|
"DBK", # Başkanlar Kurulu
|
||||||
|
|
||||||
|
# Military High Administrative Court
|
||||||
|
"AYIM", # Askeri Yüksek İdare Mahkemesi
|
||||||
|
"AYIMDK", # Askeri Yüksek İdare Mahkemesi Daireler Kurulu
|
||||||
|
"AYIMB", # Askeri Yüksek İdare Mahkemesi Başsavcılığı
|
||||||
|
"AYIM1", # Askeri Yüksek İdare Mahkemesi 1. Daire
|
||||||
|
"AYIM2", # Askeri Yüksek İdare Mahkemesi 2. Daire
|
||||||
|
"AYIM3" # Askeri Yüksek İdare Mahkemesi 3. Daire
|
||||||
|
]
|
||||||
|
|
||||||
|
# Mapping from abbreviated values to full Turkish API values
|
||||||
|
BIRIM_ADI_MAPPING = {
|
||||||
|
"ALL": None, # Will be handled specially in client
|
||||||
|
|
||||||
|
# Yargıtay Civil Chambers (1-23)
|
||||||
|
"H1": "1. Hukuk Dairesi", "H2": "2. Hukuk Dairesi", "H3": "3. Hukuk Dairesi",
|
||||||
|
"H4": "4. Hukuk Dairesi", "H5": "5. Hukuk Dairesi", "H6": "6. Hukuk Dairesi",
|
||||||
|
"H7": "7. Hukuk Dairesi", "H8": "8. Hukuk Dairesi", "H9": "9. Hukuk Dairesi",
|
||||||
|
"H10": "10. Hukuk Dairesi", "H11": "11. Hukuk Dairesi", "H12": "12. Hukuk Dairesi",
|
||||||
|
"H13": "13. Hukuk Dairesi", "H14": "14. Hukuk Dairesi", "H15": "15. Hukuk Dairesi",
|
||||||
|
"H16": "16. Hukuk Dairesi", "H17": "17. Hukuk Dairesi", "H18": "18. Hukuk Dairesi",
|
||||||
|
"H19": "19. Hukuk Dairesi", "H20": "20. Hukuk Dairesi", "H21": "21. Hukuk Dairesi",
|
||||||
|
"H22": "22. Hukuk Dairesi", "H23": "23. Hukuk Dairesi",
|
||||||
|
|
||||||
|
# Yargıtay Criminal Chambers (1-23)
|
||||||
|
"C1": "1. Ceza Dairesi", "C2": "2. Ceza Dairesi", "C3": "3. Ceza Dairesi",
|
||||||
|
"C4": "4. Ceza Dairesi", "C5": "5. Ceza Dairesi", "C6": "6. Ceza Dairesi",
|
||||||
|
"C7": "7. Ceza Dairesi", "C8": "8. Ceza Dairesi", "C9": "9. Ceza Dairesi",
|
||||||
|
"C10": "10. Ceza Dairesi", "C11": "11. Ceza Dairesi", "C12": "12. Ceza Dairesi",
|
||||||
|
"C13": "13. Ceza Dairesi", "C14": "14. Ceza Dairesi", "C15": "15. Ceza Dairesi",
|
||||||
|
"C16": "16. Ceza Dairesi", "C17": "17. Ceza Dairesi", "C18": "18. Ceza Dairesi",
|
||||||
|
"C19": "19. Ceza Dairesi", "C20": "20. Ceza Dairesi", "C21": "21. Ceza Dairesi",
|
||||||
|
"C22": "22. Ceza Dairesi", "C23": "23. Ceza Dairesi",
|
||||||
|
|
||||||
|
# Yargıtay Councils and Assemblies
|
||||||
|
"HGK": "Hukuk Genel Kurulu",
|
||||||
|
"CGK": "Ceza Genel Kurulu",
|
||||||
|
"BGK": "Büyük Genel Kurulu",
|
||||||
|
"HBK": "Hukuk Daireleri Başkanlar Kurulu",
|
||||||
|
"CBK": "Ceza Daireleri Başkanlar Kurulu",
|
||||||
|
|
||||||
|
# Danıştay Chambers (1-17)
|
||||||
|
"D1": "1. Daire", "D2": "2. Daire", "D3": "3. Daire", "D4": "4. Daire",
|
||||||
|
"D5": "5. Daire", "D6": "6. Daire", "D7": "7. Daire", "D8": "8. Daire",
|
||||||
|
"D9": "9. Daire", "D10": "10. Daire", "D11": "11. Daire", "D12": "12. Daire",
|
||||||
|
"D13": "13. Daire", "D14": "14. Daire", "D15": "15. Daire", "D16": "16. Daire",
|
||||||
|
"D17": "17. Daire",
|
||||||
|
|
||||||
|
# Danıştay Councils and Boards
|
||||||
|
"DBGK": "Büyük Gen.Kur.",
|
||||||
|
"IDDK": "İdare Dava Daireleri Kurulu",
|
||||||
|
"VDDK": "Vergi Dava Daireleri Kurulu",
|
||||||
|
"IBK": "İçtihatları Birleştirme Kurulu",
|
||||||
|
"IIK": "İdari İşler Kurulu",
|
||||||
|
"DBK": "Başkanlar Kurulu",
|
||||||
|
|
||||||
|
# Military High Administrative Court
|
||||||
|
"AYIM": "Askeri Yüksek İdare Mahkemesi",
|
||||||
|
"AYIMDK": "Askeri Yüksek İdare Mahkemesi Daireler Kurulu",
|
||||||
|
"AYIMB": "Askeri Yüksek İdare Mahkemesi Başsavcılığı",
|
||||||
|
"AYIM1": "Askeri Yüksek İdare Mahkemesi 1. Daire",
|
||||||
|
"AYIM2": "Askeri Yüksek İdare Mahkemesi 2. Daire",
|
||||||
|
"AYIM3": "Askeri Yüksek İdare Mahkemesi 3. Daire"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper function to get full Turkish name from abbreviated value
|
||||||
|
def get_full_birim_adi(abbreviated_value: str) -> str:
|
||||||
|
"""Convert abbreviated birimAdi value to full Turkish name for API calls."""
|
||||||
|
if abbreviated_value == "ALL" or not abbreviated_value:
|
||||||
|
return "" # Empty string for ALL or None
|
||||||
|
|
||||||
|
return BIRIM_ADI_MAPPING.get(abbreviated_value, abbreviated_value)
|
||||||
|
|
||||||
|
# Helper function to validate abbreviated value
|
||||||
|
def is_valid_birim_adi(abbreviated_value: str) -> bool:
|
||||||
|
"""Check if abbreviated birimAdi value is valid."""
|
||||||
|
return abbreviated_value in BIRIM_ADI_MAPPING
|
||||||
@@ -4,94 +4,33 @@ from pydantic import BaseModel, Field
|
|||||||
from typing import List, Optional, Dict, Any, Literal, Union
|
from typing import List, Optional, Dict, Any, Literal, Union
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
# Import YargitayBirimEnum for chamber filtering
|
# Import compressed BirimAdiEnum for chamber filtering
|
||||||
from yargitay_mcp_module.models import YargitayBirimEnum
|
from .enums import BirimAdiEnum
|
||||||
|
|
||||||
# Danıştay Chamber/Board Options
|
# Court Type Options for Unified Search
|
||||||
DanistayBirimEnum = Literal[
|
BedestenCourtTypeEnum = Literal[
|
||||||
"ALL", # "ALL" for all chambers
|
"YARGITAYKARARI", # Yargıtay (Court of Cassation)
|
||||||
# Main Councils
|
"DANISTAYKARAR", # Danıştay (Council of State)
|
||||||
"Büyük Gen.Kur.", # Grand General Assembly
|
"YERELHUKUK", # Local Civil Courts
|
||||||
"İdare Dava Daireleri Kurulu", # Administrative Cases Chambers Council
|
"ISTINAFHUKUK", # Civil Courts of Appeals
|
||||||
"Vergi Dava Daireleri Kurulu", # Tax Cases Chambers Council
|
"KYB" # Extraordinary Appeals (Kanun Yararına Bozma)
|
||||||
"İçtihatları Birleştirme Kurulu", # Precedents Unification Council
|
|
||||||
"İdari İşler Kurulu", # Administrative Affairs Council
|
|
||||||
"Başkanlar Kurulu", # Presidents Council
|
|
||||||
# Chambers
|
|
||||||
"1. Daire", "2. Daire", "3. Daire", "4. Daire", "5. Daire",
|
|
||||||
"6. Daire", "7. Daire", "8. Daire", "9. Daire", "10. Daire",
|
|
||||||
"11. Daire", "12. Daire", "13. Daire", "14. Daire", "15. Daire",
|
|
||||||
"16. Daire", "17. Daire",
|
|
||||||
# Military High Administrative Court
|
|
||||||
"Askeri Yüksek İdare Mahkemesi",
|
|
||||||
"Askeri Yüksek İdare Mahkemesi Daireler Kurulu",
|
|
||||||
"Askeri Yüksek İdare Mahkemesi Başsavcılığı",
|
|
||||||
"Askeri Yüksek İdare Mahkemesi 1. Daire",
|
|
||||||
"Askeri Yüksek İdare Mahkemesi 2. Daire",
|
|
||||||
"Askeri Yüksek İdare Mahkemesi 3. Daire"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Search Request Models
|
# Search Request Models
|
||||||
class BedestenSearchData(BaseModel):
|
class BedestenSearchData(BaseModel):
|
||||||
pageSize: int = Field(..., description="""Number of results per page.
|
pageSize: int = Field(..., description="Results per page (1-10)")
|
||||||
Range: 1-100 results per page
|
pageNumber: int = Field(..., description="Page number (1-indexed)")
|
||||||
Recommended: 10-50 for balanced performance
|
itemTypeList: List[str] = Field(..., description="Court type filter (YARGITAYKARARI/DANISTAYKARAR/YERELHUKUK/ISTINAFHUKUK/KYB)")
|
||||||
Higher values for comprehensive analysis""")
|
phrase: str = Field(..., description="Search phrase. Supports: 'word', \"exact phrase\", +required, -exclude, AND/OR/NOT operators. No wildcards or regex.")
|
||||||
pageNumber: int = Field(..., description="""Page number to retrieve (1-indexed).
|
birimAdi: BirimAdiEnum = Field("ALL", description="""
|
||||||
Start with 1 for first page
|
Chamber filter (optional). Abbreviated values with Turkish names:
|
||||||
Calculate total pages from response.data.total / pageSize
|
• Yargıtay: H1-H23 (1-23. Hukuk Dairesi), C1-C23 (1-23. Ceza Dairesi), HGK (Hukuk Genel Kurulu), CGK (Ceza Genel Kurulu), BGK (Büyük Genel Kurulu), HBK (Hukuk Daireleri Başkanlar Kurulu), CBK (Ceza Daireleri Başkanlar Kurulu)
|
||||||
Navigate: pageNumber=2 gets next set of results""")
|
• Danıştay: D1-D17 (1-17. Daire), DBGK (Büyük Gen.Kur.), IDDK (İdare Dava Daireleri Kurulu), VDDK (Vergi Dava Daireleri Kurulu), IBK (İçtihatları Birleştirme Kurulu), IIK (İdari İşler Kurulu), DBK (Başkanlar Kurulu), AYIM (Askeri Yüksek İdare Mahkemesi), AYIM1-3 (Askeri Yüksek İdare Mahkemesi 1-3. Daire)
|
||||||
itemTypeList: List[str] = Field(..., description="""Court type filter - determines which court decisions to search:
|
""")
|
||||||
• ["YARGITAYKARARI"]: Court of Cassation (Yargıtay) - supreme court civil/criminal decisions
|
kararTarihiStart: Optional[str] = Field(None, description="Start date (ISO 8601 format)")
|
||||||
• ["DANISTAYKARAR"]: Council of State (Danıştay) - administrative court decisions
|
kararTarihiEnd: Optional[str] = Field(None, description="End date (ISO 8601 format)")
|
||||||
• ["YERELHUKUK"]: Local Civil Courts (Yerel Hukuk Mahkemeleri) - first instance civil decisions
|
sortFields: List[str] = Field(default=["KARAR_TARIHI"], description="Sort fields")
|
||||||
• ["ISTINAFHUKUK"]: Civil Courts of Appeals (İstinaf Hukuk Mahkemeleri) - appellate court decisions
|
sortDirection: str = Field(default="desc", description="Sort direction (asc/desc)")
|
||||||
• ["KYB"]: Extraordinary Appeal (Kanun Yararına Bozma) - extraordinary appeal decisions
|
|
||||||
Note: Use single-item list for specific court type targeting""")
|
|
||||||
phrase: str = Field(..., description="""Search phrase/keyword with advanced search support:
|
|
||||||
• Regular search: "mülkiyet kararı" - searches words separately
|
|
||||||
• Exact phrase: "\"mülkiyet kararı\"" - searches exact phrase (more precise)
|
|
||||||
• Legal concepts: "\"idari işlem\"", "\"sözleşme ihlali\"", "\"tazminat davası\""
|
|
||||||
• Empty string: searches all documents (use with filters)
|
|
||||||
Exact phrases significantly reduce false positives for precise legal research""")
|
|
||||||
birimAdi: Optional[Union[YargitayBirimEnum, DanistayBirimEnum]] = Field(None, description="""
|
|
||||||
Chamber/Department (Daire) filter (optional). Available options depend on itemTypeList:
|
|
||||||
|
|
||||||
For YARGITAYKARARI - Court of Cassation (52 options):
|
|
||||||
- None/null for ALL chambers
|
|
||||||
- 'Civil General Assembly (Hukuk Genel Kurulu)', '1st Civil Chamber (1. Hukuk Dairesi)' through '23rd Civil Chamber (23. Hukuk Dairesi)'
|
|
||||||
- 'Criminal General Assembly (Ceza Genel Kurulu)', '1st Criminal Chamber (1. Ceza Dairesi)' through '23rd Criminal Chamber (23. Ceza Dairesi)'
|
|
||||||
- 'Civil Chambers Presidents Board (Hukuk Daireleri Başkanlar Kurulu)', 'Criminal Chambers Presidents Board (Ceza Daireleri Başkanlar Kurulu)'
|
|
||||||
- 'Grand General Assembly (Büyük Genel Kurulu)'
|
|
||||||
|
|
||||||
For DANISTAYKARAR - Council of State (27 options):
|
|
||||||
- None/null for ALL chambers
|
|
||||||
- 'Grand General Assembly (Büyük Gen.Kur.)', 'Administrative Cases Chambers Council (İdare Dava Daireleri Kurulu)', 'Tax Cases Chambers Council (Vergi Dava Daireleri Kurulu)'
|
|
||||||
- '1st Chamber (1. Daire)' through '17th Chamber (17. Daire)'
|
|
||||||
- 'Precedents Unification Council (İçtihatları Birleştirme Kurulu)', 'Administrative Affairs Council (İdari İşler Kurulu)', 'Presidents Council (Başkanlar Kurulu)'
|
|
||||||
- Military courts: 'Military High Administrative Court (Askeri Yüksek İdare Mahkemesi)' variants
|
|
||||||
""")
|
|
||||||
kararTarihiStart: Optional[str] = Field(None, description="""Decision start date (Karar Tarihi Başlangıç) filter (optional).
|
|
||||||
Format: YYYY-MM-DDTHH:MM:SS.000Z (ISO 8601 with Z timezone)
|
|
||||||
Examples:
|
|
||||||
• "2024-01-01T00:00:00.000Z" - from beginning of 2024
|
|
||||||
• "2023-06-15T00:00:00.000Z" - from June 15, 2023
|
|
||||||
• "2024-03-01T00:00:00.000Z" - from March 1, 2024
|
|
||||||
Use with kararTarihiEnd for date range, or alone for "from date" filtering""")
|
|
||||||
kararTarihiEnd: Optional[str] = Field(None, description="""Decision end date (Karar Tarihi Bitiş) filter (optional).
|
|
||||||
Format: YYYY-MM-DDTHH:MM:SS.000Z (ISO 8601 with Z timezone)
|
|
||||||
Examples:
|
|
||||||
• "2024-12-31T23:59:59.999Z" - until end of 2024
|
|
||||||
• "2023-12-31T23:59:59.999Z" - until end of 2023
|
|
||||||
• "2024-06-30T23:59:59.999Z" - until end of June 2024
|
|
||||||
Use with kararTarihiStart for date range, or alone for "until date" filtering""")
|
|
||||||
sortFields: List[str] = Field(default=["KARAR_TARIHI"], description="""Sorting field (Sıralama Alanı) specification.
|
|
||||||
["KARAR_TARIHI"]: Sort by decision date (Karar Tarihi) [DEFAULT]
|
|
||||||
Most common use case for chronological ordering""")
|
|
||||||
sortDirection: str = Field(default="desc", description="""Sort direction (Sıralama Yönü) for results.
|
|
||||||
"desc": Descending order - newest decisions first [DEFAULT]
|
|
||||||
"asc": Ascending order - oldest decisions first
|
|
||||||
Recommended: "desc" for latest legal developments""")
|
|
||||||
|
|
||||||
class BedestenSearchRequest(BaseModel):
|
class BedestenSearchRequest(BaseModel):
|
||||||
data: BedestenSearchData
|
data: BedestenSearchData
|
||||||
@@ -125,7 +64,7 @@ class BedestenSearchDataResponse(BaseModel):
|
|||||||
start: int
|
start: int
|
||||||
|
|
||||||
class BedestenSearchResponse(BaseModel):
|
class BedestenSearchResponse(BaseModel):
|
||||||
data: BedestenSearchDataResponse
|
data: Optional[BedestenSearchDataResponse]
|
||||||
metadata: Dict[str, Any]
|
metadata: Dict[str, Any]
|
||||||
|
|
||||||
# Document Request/Response Models
|
# Document Request/Response Models
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from fastmcp import Client
|
||||||
|
from mcp_server_main import app
|
||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
async def check_response_format():
|
||||||
|
client = Client(app)
|
||||||
|
async with client:
|
||||||
|
result = await client.call_tool('search_bedesten_unified', {
|
||||||
|
'phrase': 'mülkiyet',
|
||||||
|
'court_types': ['YARGITAYKARARI'],
|
||||||
|
'birimAdi': 'H1',
|
||||||
|
'pageSize': 3
|
||||||
|
})
|
||||||
|
if result and result.content:
|
||||||
|
data = json.loads(result.content[0].text)
|
||||||
|
print('Response keys:', list(data.keys()))
|
||||||
|
print('Sample response:', json.dumps(data, indent=2, ensure_ascii=False)[:500])
|
||||||
|
|
||||||
|
asyncio.run(check_response_format())
|
||||||
@@ -76,12 +76,16 @@ class DanistayApiClient:
|
|||||||
mevzuatNumarasi=params.mevzuatNumarasi or "",
|
mevzuatNumarasi=params.mevzuatNumarasi or "",
|
||||||
mevzuatAdi=params.mevzuatAdi or "",
|
mevzuatAdi=params.mevzuatAdi or "",
|
||||||
madde=params.madde or "",
|
madde=params.madde or "",
|
||||||
siralama=params.siralama,
|
siralama="1",
|
||||||
siralamaDirection=params.siralamaDirection,
|
siralamaDirection="desc",
|
||||||
pageSize=params.pageSize,
|
pageSize=params.pageSize,
|
||||||
pageNumber=params.pageNumber
|
pageNumber=params.pageNumber
|
||||||
)
|
)
|
||||||
final_payload = {"data": data_for_payload.model_dump(exclude_defaults=False, exclude_none=False)}
|
# Create request dict and remove empty string fields to avoid API issues
|
||||||
|
payload_dict = data_for_payload.model_dump(exclude_defaults=False, exclude_none=False)
|
||||||
|
# Remove empty string fields that might cause API issues
|
||||||
|
cleaned_payload = {k: v for k, v in payload_dict.items() if v != ""}
|
||||||
|
final_payload = {"data": cleaned_payload}
|
||||||
logger.info(f"DanistayApiClient: Performing DETAILED search via {self.DETAILED_SEARCH_ENDPOINT} with payload: {final_payload}")
|
logger.info(f"DanistayApiClient: Performing DETAILED search via {self.DETAILED_SEARCH_ENDPOINT} with payload: {final_payload}")
|
||||||
return await self._execute_api_search(self.DETAILED_SEARCH_ENDPOINT, final_payload)
|
return await self._execute_api_search(self.DETAILED_SEARCH_ENDPOINT, final_payload)
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from typing import List, Optional, Dict, Any
|
|||||||
|
|
||||||
class DanistayBaseSearchRequest(BaseModel):
|
class DanistayBaseSearchRequest(BaseModel):
|
||||||
"""Base model for common search parameters for Danistay."""
|
"""Base model for common search parameters for Danistay."""
|
||||||
pageSize: int = Field(default=10, ge=1, le=100)
|
pageSize: int = Field(default=10, ge=1, le=10)
|
||||||
pageNumber: int = Field(default=1, ge=1)
|
pageNumber: int = Field(default=1, ge=1)
|
||||||
# siralama and siralamaDirection are part of detailed search, not necessarily keyword search
|
# siralama and siralamaDirection are part of detailed search, not necessarily keyword search
|
||||||
# as per user's provided payloads.
|
# as per user's provided payloads.
|
||||||
@@ -21,11 +21,11 @@ class DanistayKeywordSearchRequestData(BaseModel):
|
|||||||
|
|
||||||
class DanistayKeywordSearchRequest(BaseModel): # This is the model the MCP tool will accept
|
class DanistayKeywordSearchRequest(BaseModel): # This is the model the MCP tool will accept
|
||||||
"""Model for keyword-based search request for Danistay."""
|
"""Model for keyword-based search request for Danistay."""
|
||||||
andKelimeler: List[str] = Field(default_factory=list, description="Keywords for AND logic (VE Mantığı), e.g., ['word1', 'word2']")
|
andKelimeler: List[str] = Field(default_factory=list, description="AND keywords")
|
||||||
orKelimeler: List[str] = Field(default_factory=list, description="Keywords for OR logic (VEYA Mantığı).")
|
orKelimeler: List[str] = Field(default_factory=list, description="OR keywords")
|
||||||
notAndKelimeler: List[str] = Field(default_factory=list, description="Keywords for NOT AND logic (VE DEĞİL Mantığı).")
|
notAndKelimeler: List[str] = Field(default_factory=list, description="NOT AND keywords")
|
||||||
notOrKelimeler: List[str] = Field(default_factory=list, description="Keywords for NOT OR logic (VEYA DEĞİL Mantığı).")
|
notOrKelimeler: List[str] = Field(default_factory=list, description="NOT OR keywords")
|
||||||
pageSize: int = Field(default=10, ge=1, le=100)
|
pageSize: int = Field(default=10, ge=1, le=10)
|
||||||
pageNumber: int = Field(default=1, ge=1)
|
pageNumber: int = Field(default=1, ge=1)
|
||||||
|
|
||||||
class DanistayDetailedSearchRequestData(BaseModel): # Internal data model for detailed search payload
|
class DanistayDetailedSearchRequestData(BaseModel): # Internal data model for detailed search payload
|
||||||
@@ -51,20 +51,18 @@ class DanistayDetailedSearchRequestData(BaseModel): # Internal data model for de
|
|||||||
|
|
||||||
class DanistayDetailedSearchRequest(DanistayBaseSearchRequest): # MCP tool will accept this
|
class DanistayDetailedSearchRequest(DanistayBaseSearchRequest): # MCP tool will accept this
|
||||||
"""Model for detailed search request for Danistay."""
|
"""Model for detailed search request for Danistay."""
|
||||||
daire: Optional[str] = Field(None, description="Chamber/Department name (e.g., '1. Daire').")
|
daire: str = Field("", description="Chamber")
|
||||||
esasYil: Optional[str] = Field(None, description="Case year for 'Esas No'.")
|
esasYil: str = Field("", description="Case year")
|
||||||
esasIlkSiraNo: Optional[str] = Field(None, description="Starting sequence for 'Esas No'.")
|
esasIlkSiraNo: str = Field("", description="Start case no")
|
||||||
esasSonSiraNo: Optional[str] = Field(None, description="Ending sequence for 'Esas No'.")
|
esasSonSiraNo: str = Field("", description="End case no")
|
||||||
kararYil: Optional[str] = Field(None, description="Decision year for 'Karar No'.")
|
kararYil: str = Field("", description="Decision year")
|
||||||
kararIlkSiraNo: Optional[str] = Field(None, description="Starting sequence for 'Karar No'.")
|
kararIlkSiraNo: str = Field("", description="Start decision no")
|
||||||
kararSonSiraNo: Optional[str] = Field(None, description="Ending sequence for 'Karar No'.")
|
kararSonSiraNo: str = Field("", description="End decision no")
|
||||||
baslangicTarihi: Optional[str] = Field(None, description="Start date for decision (DD.MM.YYYY).")
|
baslangicTarihi: str = Field("", description="Start date")
|
||||||
bitisTarihi: Optional[str] = Field(None, description="End date for decision (DD.MM.YYYY).")
|
bitisTarihi: str = Field("", description="End date")
|
||||||
mevzuatNumarasi: Optional[str] = Field(None, description="Legislation number.")
|
mevzuatNumarasi: str = Field("", description="Law number")
|
||||||
mevzuatAdi: Optional[str] = Field(None, description="Legislation name.")
|
mevzuatAdi: str = Field("", description="Law name")
|
||||||
madde: Optional[str] = Field(None, description="Article number.")
|
madde: str = Field("", description="Article")
|
||||||
siralama: str = Field("1", description="Sorting criteria (e.g., 1: Esas No, 3: Karar Tarihi).")
|
|
||||||
siralamaDirection: str = Field("desc", description="Sorting direction ('asc' or 'desc').")
|
|
||||||
# Add a general keyword field if detailed search also supports it
|
# Add a general keyword field if detailed search also supports it
|
||||||
# arananKelime: Optional[str] = Field(None, description="General keyword for detailed search.")
|
# arananKelime: Optional[str] = Field(None, description="General keyword for detailed search.")
|
||||||
|
|
||||||
@@ -76,15 +74,15 @@ class DanistayApiDecisionEntry(BaseModel):
|
|||||||
id: str
|
id: str
|
||||||
# The API response for keyword search uses "daireKurul", detailed search example uses "daire".
|
# The API response for keyword search uses "daireKurul", detailed search example uses "daire".
|
||||||
# We use an alias to handle both and map to a consistent field name "chamber".
|
# We use an alias to handle both and map to a consistent field name "chamber".
|
||||||
chamber: Optional[str] = Field(None, alias="daire", description="The chamber or board.")
|
chamber: str = Field("", alias="daire", description="Chamber")
|
||||||
esasNo: Optional[str] = Field(None)
|
esasNo: str = Field("", description="Case number")
|
||||||
kararNo: Optional[str] = Field(None)
|
kararNo: str = Field("", description="Decision number")
|
||||||
kararTarihi: Optional[str] = Field(None)
|
kararTarihi: str = Field("", description="Decision date")
|
||||||
arananKelime: Optional[str] = Field(None, description="Matched keyword (Aranan Kelime) if provided in response.")
|
arananKelime: str = Field("", description="Keyword")
|
||||||
# index: Optional[int] = None # Present in response, can be added if needed by MCP tool
|
# index: Optional[int] = None # Present in response, can be added if needed by MCP tool
|
||||||
# siraNo: Optional[int] = None # Present in detailed response, can be added
|
# siraNo: Optional[int] = None # Present in detailed response, can be added
|
||||||
|
|
||||||
document_url: Optional[HttpUrl] = Field(None, description="URL (Belge URL) to the full document, constructed by the client.")
|
document_url: Optional[HttpUrl] = Field(None, description="Document URL")
|
||||||
|
|
||||||
model_config = ConfigDict(populate_by_name=True, extra='ignore') # Important for alias to work and ignore extra fields
|
model_config = ConfigDict(populate_by_name=True, extra='ignore') # Important for alias to work and ignore extra fields
|
||||||
|
|
||||||
@@ -93,7 +91,7 @@ class DanistayApiResponseInnerData(BaseModel):
|
|||||||
data: List[DanistayApiDecisionEntry]
|
data: List[DanistayApiDecisionEntry]
|
||||||
recordsTotal: int
|
recordsTotal: int
|
||||||
recordsFiltered: int
|
recordsFiltered: int
|
||||||
draw: Optional[int] = Field(None, description="Draw counter (Çizim Sayıcısı) from API, usually for DataTables.")
|
draw: int = Field(0, description="Draw counter")
|
||||||
|
|
||||||
class DanistayApiResponse(BaseModel):
|
class DanistayApiResponse(BaseModel):
|
||||||
"""Model for the complete search response from the Danistay API."""
|
"""Model for the complete search response from the Danistay API."""
|
||||||
@@ -103,7 +101,7 @@ class DanistayApiResponse(BaseModel):
|
|||||||
class DanistayDocumentMarkdown(BaseModel):
|
class DanistayDocumentMarkdown(BaseModel):
|
||||||
"""Model for a Danistay decision document, containing only Markdown content."""
|
"""Model for a Danistay decision document, containing only Markdown content."""
|
||||||
id: str
|
id: str
|
||||||
markdown_content: Optional[str] = Field(None, description="The decision content (Karar İçeriği) converted to Markdown.")
|
markdown_content: str = Field("", description="The decision content (Karar İçeriği) converted to Markdown.")
|
||||||
source_url: HttpUrl
|
source_url: HttpUrl
|
||||||
|
|
||||||
class CompactDanistaySearchResult(BaseModel):
|
class CompactDanistaySearchResult(BaseModel):
|
||||||
|
|||||||
@@ -63,7 +63,11 @@ class EmsalApiClient:
|
|||||||
pageNumber=params.page_number
|
pageNumber=params.page_number
|
||||||
)
|
)
|
||||||
|
|
||||||
final_payload = {"data": data_for_api_payload.model_dump(by_alias=True, exclude_none=True)}
|
# Create request dict and remove empty string fields to avoid API issues
|
||||||
|
payload_dict = data_for_api_payload.model_dump(by_alias=True, exclude_none=True)
|
||||||
|
# Remove empty string fields that might cause API issues
|
||||||
|
cleaned_payload = {k: v for k, v in payload_dict.items() if v != ""}
|
||||||
|
final_payload = {"data": cleaned_payload}
|
||||||
|
|
||||||
logger.info(f"EmsalApiClient: Performing DETAILED search with payload: {final_payload}")
|
logger.info(f"EmsalApiClient: Performing DETAILED search with payload: {final_payload}")
|
||||||
return await self._execute_api_search(self.DETAILED_SEARCH_ENDPOINT, final_payload)
|
return await self._execute_api_search(self.DETAILED_SEARCH_ENDPOINT, final_payload)
|
||||||
|
|||||||
+27
-27
@@ -12,12 +12,12 @@ class EmsalDetailedSearchRequestData(BaseModel):
|
|||||||
"""
|
"""
|
||||||
arananKelime: Optional[str] = ""
|
arananKelime: Optional[str] = ""
|
||||||
|
|
||||||
Bam_Hukuk_Mahkemeleri: Optional[str] = Field(None, alias="Bam Hukuk Mahkemeleri")
|
Bam_Hukuk_Mahkemeleri: str = Field("", alias="Bam Hukuk Mahkemeleri")
|
||||||
Hukuk_Mahkemeleri: Optional[str] = Field(None, alias="Hukuk Mahkemeleri")
|
Hukuk_Mahkemeleri: str = Field("", alias="Hukuk Mahkemeleri")
|
||||||
# Add other specific court type fields from the form if they are separate keys in payload
|
# Add other specific court type fields from the form if they are separate keys in payload
|
||||||
# E.g., "Ceza Mahkemeleri", "İdari Mahkemeler" etc.
|
# E.g., "Ceza Mahkemeleri", "İdari Mahkemeler" etc.
|
||||||
|
|
||||||
birimHukukMah: Optional[str] = Field("", description="List of selected Regional Civil Chambers (Bölge Hukuk Mahkemeleri), '+' separated.")
|
birimHukukMah: Optional[str] = Field("", description="Regional chambers (+ separated)")
|
||||||
|
|
||||||
esasYil: Optional[str] = ""
|
esasYil: Optional[str] = ""
|
||||||
esasIlkSiraNo: Optional[str] = ""
|
esasIlkSiraNo: Optional[str] = ""
|
||||||
@@ -36,42 +36,42 @@ class EmsalDetailedSearchRequestData(BaseModel):
|
|||||||
|
|
||||||
class EmsalSearchRequest(BaseModel): # This is the model the MCP tool will accept
|
class EmsalSearchRequest(BaseModel): # This is the model the MCP tool will accept
|
||||||
"""Model for Emsal detailed search request, with user-friendly field names."""
|
"""Model for Emsal detailed search request, with user-friendly field names."""
|
||||||
keyword: Optional[str] = Field(None, description="Keyword (Anahtar Kelime) to search.")
|
keyword: str = Field("", description="Keyword")
|
||||||
|
|
||||||
selected_bam_civil_court: Optional[str] = Field(None, description="Selected BAM Civil Court (Seçilen BAM Hukuk Mahkemesi) (maps to 'Bam Hukuk Mahkemeleri' payload key).")
|
selected_bam_civil_court: str = Field("", description="BAM Civil Court")
|
||||||
selected_civil_court: Optional[str] = Field(None, description="Selected Civil Court (Seçilen Hukuk Mahkemesi) (maps to 'Hukuk Mahkemeleri' payload key).")
|
selected_civil_court: str = Field("", description="Civil Court")
|
||||||
selected_regional_civil_chambers: Optional[List[str]] = Field(default_factory=list, description="Selected Regional Civil Chambers (Seçilen Bölge Hukuk Daireleri) (for 'birimHukukMah', joined by '+').")
|
selected_regional_civil_chambers: List[str] = Field(default_factory=list, description="Regional chambers")
|
||||||
|
|
||||||
case_year_esas: Optional[str] = Field(None, description="Case year (Dava Yılı) for 'Esas No'.")
|
case_year_esas: str = Field("", description="Case year")
|
||||||
case_start_seq_esas: Optional[str] = Field(None, description="Starting sequence (Başlangıç Sırası) for 'Esas No'.")
|
case_start_seq_esas: str = Field("", description="Start case no")
|
||||||
case_end_seq_esas: Optional[str] = Field(None, description="Ending sequence (Bitiş Sırası) for 'Esas No'.")
|
case_end_seq_esas: str = Field("", description="End case no")
|
||||||
|
|
||||||
decision_year_karar: Optional[str] = Field(None, description="Decision year (Karar Yılı) for 'Karar No'.")
|
decision_year_karar: str = Field("", description="Decision year")
|
||||||
decision_start_seq_karar: Optional[str] = Field(None, description="Starting sequence (Başlangıç Sırası) for 'Karar No'.")
|
decision_start_seq_karar: str = Field("", description="Start decision no")
|
||||||
decision_end_seq_karar: Optional[str] = Field(None, description="Ending sequence (Bitiş Sırası) for 'Karar No'.")
|
decision_end_seq_karar: str = Field("", description="End decision no")
|
||||||
|
|
||||||
start_date: Optional[str] = Field(None, description="Start date (Başlangıç Tarihi) for decision (DD.MM.YYYY).")
|
start_date: str = Field("", description="Start date (DD.MM.YYYY)")
|
||||||
end_date: Optional[str] = Field(None, description="End date (Bitiş Tarihi) for decision (DD.MM.YYYY).")
|
end_date: str = Field("", description="End date (DD.MM.YYYY)")
|
||||||
|
|
||||||
sort_criteria: str = Field("1", description="Sorting criteria (Sıralama Kriteri) (e.g., 1: Esas No).")
|
sort_criteria: str = Field("1", description="Sort by")
|
||||||
sort_direction: str = Field("desc", description="Sorting direction (Sıralama Yönü) ('asc' or 'desc').")
|
sort_direction: str = Field("desc", description="Direction")
|
||||||
|
|
||||||
page_number: int = Field(default=1, ge=1)
|
page_number: int = Field(default=1, ge=1)
|
||||||
page_size: int = Field(default=10, ge=1, le=100)
|
page_size: int = Field(default=10, ge=1, le=10)
|
||||||
|
|
||||||
|
|
||||||
class EmsalApiDecisionEntry(BaseModel):
|
class EmsalApiDecisionEntry(BaseModel):
|
||||||
"""Model for an individual decision entry from the Emsal API search response."""
|
"""Model for an individual decision entry from the Emsal API search response."""
|
||||||
id: str
|
id: str
|
||||||
daire: Optional[str] = Field(None, description="The chamber/court (Daire/Mahkeme) that made the decision.")
|
daire: str = Field("", description="Chamber")
|
||||||
esasNo: Optional[str] = Field(None)
|
esasNo: str = Field("", description="Case number")
|
||||||
kararNo: Optional[str] = Field(None)
|
kararNo: str = Field("", description="Decision number")
|
||||||
kararTarihi: Optional[str] = Field(None)
|
kararTarihi: str = Field("", description="Decision date")
|
||||||
arananKelime: Optional[str] = Field(None, description="Matched keyword (Aranan Kelime) from the search.")
|
arananKelime: str = Field("", description="Keyword")
|
||||||
durum: Optional[str] = Field(None, description="Status (Durum) of the decision (e.g., 'KESİNLEŞMEDİ').")
|
durum: str = Field("", description="Status")
|
||||||
# index: Optional[int] = None # Present in Emsal response, can be added if tool needs it
|
# index: Optional[int] = None # Present in Emsal response, can be added if tool needs it
|
||||||
|
|
||||||
document_url: Optional[HttpUrl] = Field(None, description="URL (Belge URL) to the full document, constructed by the client.")
|
document_url: Optional[HttpUrl] = Field(None, description="Document URL")
|
||||||
|
|
||||||
model_config = ConfigDict(extra='ignore')
|
model_config = ConfigDict(extra='ignore')
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ class EmsalApiResponseInnerData(BaseModel):
|
|||||||
data: List[EmsalApiDecisionEntry]
|
data: List[EmsalApiDecisionEntry]
|
||||||
recordsTotal: int
|
recordsTotal: int
|
||||||
recordsFiltered: int
|
recordsFiltered: int
|
||||||
draw: Optional[int] = Field(None, description="Draw counter (Çizim Sayıcısı) from API, usually for DataTables.")
|
draw: int = Field(0, description="Draw counter (Çizim Sayıcısı) from API, usually for DataTables.")
|
||||||
|
|
||||||
class EmsalApiResponse(BaseModel):
|
class EmsalApiResponse(BaseModel):
|
||||||
"""Model for the complete search response from the Emsal API."""
|
"""Model for the complete search response from the Emsal API."""
|
||||||
@@ -90,7 +90,7 @@ class EmsalApiResponse(BaseModel):
|
|||||||
class EmsalDocumentMarkdown(BaseModel):
|
class EmsalDocumentMarkdown(BaseModel):
|
||||||
"""Model for an Emsal decision document, containing only Markdown content."""
|
"""Model for an Emsal decision document, containing only Markdown content."""
|
||||||
id: str
|
id: str
|
||||||
markdown_content: Optional[str] = Field(None, description="The decision content (Karar İçeriği) converted to Markdown.")
|
markdown_content: str = Field("", description="The decision content (Karar İçeriği) converted to Markdown.")
|
||||||
source_url: HttpUrl
|
source_url: HttpUrl
|
||||||
|
|
||||||
class CompactEmsalSearchResult(BaseModel):
|
class CompactEmsalSearchResult(BaseModel):
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ LOG_LEVEL = "info"
|
|||||||
[http_service]
|
[http_service]
|
||||||
internal_port = 8000
|
internal_port = 8000
|
||||||
force_https = true
|
force_https = true
|
||||||
auto_stop_machines = 'stop'
|
auto_stop_machines = 'off'
|
||||||
auto_start_machines = true
|
auto_start_machines = true
|
||||||
min_machines_running = 0
|
min_machines_running = 1
|
||||||
processes = ['app']
|
processes = ['app']
|
||||||
|
|
||||||
[[vm]]
|
[[vm]]
|
||||||
|
|||||||
@@ -648,6 +648,12 @@ class KikApiClient:
|
|||||||
rows = table.find_all("tr")
|
rows = table.find_all("tr")
|
||||||
logger.info(f"Found {len(rows)} rows in results table")
|
logger.info(f"Found {len(rows)} rows in results table")
|
||||||
|
|
||||||
|
# Debug: Log first few rows structure
|
||||||
|
for i, row in enumerate(rows[:3]):
|
||||||
|
cells = row.find_all(["td", "th"])
|
||||||
|
cell_texts = [cell.get_text(strip=True)[:30] for cell in cells]
|
||||||
|
logger.info(f"Row {i} structure: {len(cells)} cells: {cell_texts}")
|
||||||
|
|
||||||
for row_idx, row in enumerate(rows):
|
for row_idx, row in enumerate(rows):
|
||||||
# Skip first row (search bar with colspan=7) and second row (header with 6 cells)
|
# Skip first row (search bar with colspan=7) and second row (header with 6 cells)
|
||||||
if row_idx < 2:
|
if row_idx < 2:
|
||||||
@@ -803,7 +809,10 @@ class KikApiClient:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error parsing row {row_idx}: {e}", exc_info=True)
|
logger.error(f"Error parsing row {row_idx}: {e}", exc_info=True)
|
||||||
else:
|
else:
|
||||||
logger.debug(f"Row {row_idx}: Expected at least 5 cells but found {len(cells)}, skipping")
|
logger.warning(f"Row {row_idx}: Expected at least 5 cells but found {len(cells)}, skipping")
|
||||||
|
if len(cells) > 0:
|
||||||
|
cell_texts = [cell.get_text(strip=True)[:50] for cell in cells[:3]]
|
||||||
|
logger.debug(f"Row {row_idx} cells preview: {cell_texts}")
|
||||||
|
|
||||||
logger.info(f"Parsed {len(entries)} decision entries from {len(rows)} rows")
|
logger.info(f"Parsed {len(entries)} decision entries from {len(rows)} rows")
|
||||||
return entries
|
return entries
|
||||||
@@ -901,6 +910,9 @@ class KikApiClient:
|
|||||||
logger.info(f"Performing human-like search button click...")
|
logger.info(f"Performing human-like search button click...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Hide datepicker first to prevent interference
|
||||||
|
await page.evaluate("$('#ui-datepicker-div').hide()")
|
||||||
|
|
||||||
# FAST Human-like click on search button
|
# FAST Human-like click on search button
|
||||||
await self._human_click(search_button_selector, wait_before=True, wait_after=False, fast_mode=True)
|
await self._human_click(search_button_selector, wait_before=True, wait_after=False, fast_mode=True)
|
||||||
|
|
||||||
@@ -909,7 +921,8 @@ class KikApiClient:
|
|||||||
logger.info("Search navigation completed successfully")
|
logger.info("Search navigation completed successfully")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Human click failed, falling back to JavaScript: {e}")
|
logger.warning(f"Human click failed, falling back to JavaScript: {e}")
|
||||||
# Fallback to original method
|
# Hide datepicker and use JavaScript fallback
|
||||||
|
await page.evaluate("$('#ui-datepicker-div').hide()")
|
||||||
async with page.expect_navigation(wait_until="networkidle", timeout=self.request_timeout):
|
async with page.expect_navigation(wait_until="networkidle", timeout=self.request_timeout):
|
||||||
await page.evaluate(f"javascript:__doPostBack('{event_target_for_submit}','')")
|
await page.evaluate(f"javascript:__doPostBack('{event_target_for_submit}','')")
|
||||||
logger.info("Search navigation completed via fallback")
|
logger.info("Search navigation completed via fallback")
|
||||||
|
|||||||
+30
-31
@@ -12,29 +12,29 @@ class KikKararTipi(str, Enum):
|
|||||||
|
|
||||||
class KikSearchRequest(BaseModel):
|
class KikSearchRequest(BaseModel):
|
||||||
"""Model for KIK Decision search criteria."""
|
"""Model for KIK Decision search criteria."""
|
||||||
karar_tipi: KikKararTipi = Field(KikKararTipi.UYUSMAZLIK, description="Type of KIK Decision.")
|
karar_tipi: KikKararTipi = Field(KikKararTipi.UYUSMAZLIK, description="Type")
|
||||||
karar_no: Optional[str] = Field(None, description="Decision Number (e.g., '2024/UH.II-1766').")
|
karar_no: str = Field("", description="No")
|
||||||
karar_tarihi_baslangic: Optional[str] = Field(None, description="Decision Date Start (DD.MM.YYYY).", pattern=r"^\d{2}\.\d{2}\.\d{4}$")
|
karar_tarihi_baslangic: str = Field("", description="Start", 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}$")
|
karar_tarihi_bitis: str = Field("", description="End", pattern=r"^\d{2}\.\d{2}\.\d{4}$|^$")
|
||||||
resmi_gazete_sayisi: Optional[str] = Field(None, description="Official Gazette Number.")
|
resmi_gazete_sayisi: str = Field("", description="Gazette")
|
||||||
resmi_gazete_tarihi: Optional[str] = Field(None, description="Official Gazette Date (DD.MM.YYYY).", pattern=r"^\d{2}\.\d{2}\.\d{4}$")
|
resmi_gazete_tarihi: str = Field("", description="Date", pattern=r"^\d{2}\.\d{2}\.\d{4}$|^$")
|
||||||
basvuru_konusu_ihale: Optional[str] = Field(None, description="Tender subject of the application.")
|
basvuru_konusu_ihale: str = Field("", description="Subject")
|
||||||
basvuru_sahibi: Optional[str] = Field(None, description="Applicant.")
|
basvuru_sahibi: str = Field("", description="Applicant")
|
||||||
ihaleyi_yapan_idare: Optional[str] = Field(None, description="Procuring Entity.")
|
ihaleyi_yapan_idare: str = Field("", description="Entity")
|
||||||
yil: Optional[str] = Field(None, description="Year of the decision.")
|
yil: str = Field("", description="Year")
|
||||||
karar_metni: Optional[str] = Field(None, description="Keyword/phrase in decision text.")
|
karar_metni: str = Field("", description="Text")
|
||||||
page: int = Field(1, ge=1, description="Results page number.")
|
page: int = Field(1, ge=1, description="Page")
|
||||||
|
|
||||||
class KikDecisionEntry(BaseModel):
|
class KikDecisionEntry(BaseModel):
|
||||||
"""Represents a single decision entry from KIK search results."""
|
"""Represents a single decision entry from KIK search results."""
|
||||||
preview_event_target: str = Field(..., description="Internal event target for fetching details.")
|
preview_event_target: str = Field(..., description="Event target")
|
||||||
karar_no_str: str = Field(..., alias="kararNo", description="Raw decision number as extracted from KIK (e.g., '2024/UH.II-1766').")
|
karar_no_str: str = Field(..., alias="kararNo", description="Decision number")
|
||||||
karar_tipi: KikKararTipi = Field(..., description="The type of decision this entry belongs to.")
|
karar_tipi: KikKararTipi = Field(..., description="Decision type")
|
||||||
|
|
||||||
karar_tarihi_str: str = Field(..., alias="kararTarihi", description="Decision date.")
|
karar_tarihi_str: str = Field(..., alias="kararTarihi", description="Date")
|
||||||
idare_str: Optional[str] = Field(None, alias="idare", description="Procuring entity.")
|
idare_str: str = Field("", alias="idare", description="Entity")
|
||||||
basvuru_sahibi_str: Optional[str] = Field(None, alias="basvuruSahibi", description="Applicant.")
|
basvuru_sahibi_str: str = Field("", alias="basvuruSahibi", description="Applicant")
|
||||||
ihale_konusu_str: Optional[str] = Field(None, alias="ihaleKonusu", description="Tender subject.")
|
ihale_konusu_str: str = Field("", alias="ihaleKonusu", description="Subject")
|
||||||
|
|
||||||
@computed_field
|
@computed_field
|
||||||
@property
|
@property
|
||||||
@@ -58,18 +58,17 @@ class KikDocumentMarkdown(BaseModel):
|
|||||||
"""
|
"""
|
||||||
KIK decision document, with Markdown content potentially paginated.
|
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.")
|
retrieved_with_karar_id: Optional[str] = Field(None, description="Request ID")
|
||||||
# Decode edilmiş karar no ve tipini de yanıt olarak ekleyelim, Claude için faydalı olabilir.
|
retrieved_karar_no: Optional[str] = Field(None, description="Decision number")
|
||||||
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="Decision type")
|
||||||
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).")
|
karar_id_param_from_url: Optional[str] = Field(None, alias="kararIdParam", description="Internal ID")
|
||||||
markdown_chunk: Optional[str] = Field(None, description="The requested chunk of the decision content converted to Markdown.")
|
markdown_chunk: Optional[str] = Field(None, description="Content")
|
||||||
source_url: Optional[str] = Field(None, description="The source URL of the original document (KurulKararGoster.aspx).")
|
source_url: Optional[str] = Field(None, description="Source URL")
|
||||||
error_message: Optional[str] = Field(None, description="Error message if document retrieval or processing failed.")
|
error_message: Optional[str] = Field(None, description="Error")
|
||||||
current_page: int = Field(1, description="The current page number of the markdown chunk being returned.")
|
current_page: int = Field(1, description="Page")
|
||||||
total_pages: int = Field(1, description="The total number of pages the full markdown content is divided into.")
|
total_pages: int = Field(1, description="Total pages")
|
||||||
is_paginated: bool = Field(False, description="True if the full markdown content is split into multiple pages.")
|
is_paginated: bool = Field(False, description="Paginated")
|
||||||
full_content_char_count: Optional[int] = Field(None, description="Total character count of the full markdown content before chunking.")
|
full_content_char_count: Optional[int] = Field(None, description="Char count")
|
||||||
|
|
||||||
model_config = ConfigDict(populate_by_name=True)
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
@@ -11,21 +11,21 @@ class KvkkSearchRequest(BaseModel):
|
|||||||
Examples: "açık rıza", "veri güvenliği", "kişisel veri işleme"
|
Examples: "açık rıza", "veri güvenliği", "kişisel veri işleme"
|
||||||
""")
|
""")
|
||||||
page: int = Field(1, ge=1, le=50, description="Page number for search results (1-50).")
|
page: int = Field(1, ge=1, le=50, description="Page number for search results (1-50).")
|
||||||
pageSize: int = Field(10, ge=1, le=20, description="Number of results per page (1-20).")
|
pageSize: int = Field(10, ge=1, le=10, description="Number of results per page (1-10).")
|
||||||
|
|
||||||
class KvkkDecisionSummary(BaseModel):
|
class KvkkDecisionSummary(BaseModel):
|
||||||
"""Model for a single KVKK decision summary from Brave search results."""
|
"""Model for a single KVKK decision summary from Brave search results."""
|
||||||
title: Optional[str] = Field(None, description="Decision title from search results.")
|
title: Optional[str] = Field(None, description="Decision title from search results.")
|
||||||
url: Optional[HttpUrl] = Field(None, description="URL to the KVKK decision page.")
|
url: Optional[HttpUrl] = Field(None, description="URL to the KVKK decision page.")
|
||||||
description: Optional[str] = Field(None, description="Brief description or snippet from search results.")
|
description: Optional[str] = Field(None, description="Brief description or snippet from search results.")
|
||||||
decision_id: Optional[str] = Field(None, description="Extracted decision ID from URL (e.g., Icerik/7288/2021-1303).")
|
decision_id: Optional[str] = Field(None, description="Value")
|
||||||
publication_date: Optional[str] = Field(None, description="Publication date if extractable from title or description.")
|
publication_date: Optional[str] = Field(None, description="Value")
|
||||||
decision_number: Optional[str] = Field(None, description="Decision number if extractable from title or description.")
|
decision_number: Optional[str] = Field(None, description="Value")
|
||||||
|
|
||||||
class KvkkSearchResult(BaseModel):
|
class KvkkSearchResult(BaseModel):
|
||||||
"""Model for the overall search result for KVKK decisions."""
|
"""Model for the overall search result for KVKK decisions."""
|
||||||
decisions: List[KvkkDecisionSummary] = Field(default_factory=list, description="List of KVKK decisions found.")
|
decisions: List[KvkkDecisionSummary] = Field(default_factory=list, description="List of KVKK decisions found.")
|
||||||
total_results: Optional[int] = Field(None, description="Total number of results available (if provided by Brave API).")
|
total_results: Optional[int] = Field(None, description="Value")
|
||||||
page: int = Field(1, description="Current page number of results.")
|
page: int = Field(1, description="Current page number of results.")
|
||||||
pageSize: int = Field(10, description="Number of results per page.")
|
pageSize: int = Field(10, description="Number of results per page.")
|
||||||
query: Optional[str] = Field(None, description="The actual search query sent to Brave API.")
|
query: Optional[str] = Field(None, description="The actual search query sent to Brave API.")
|
||||||
@@ -41,7 +41,7 @@ class KvkkDocumentMarkdown(BaseModel):
|
|||||||
current_page: int = Field(description="The current page number of the markdown chunk (1-indexed).")
|
current_page: int = Field(description="The current page number of the markdown chunk (1-indexed).")
|
||||||
total_pages: int = Field(description="Total number of pages for the full markdown content.")
|
total_pages: int = Field(description="Total number of pages for the full markdown content.")
|
||||||
is_paginated: bool = Field(description="True if the full markdown content is split into multiple pages.")
|
is_paginated: bool = Field(description="True if the full markdown content is split into multiple pages.")
|
||||||
error_message: Optional[str] = Field(None, description="Error message if document retrieval or conversion failed.")
|
error_message: Optional[str] = Field(None, description="Value")
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
json_encoders = {
|
json_encoders = {
|
||||||
|
|||||||
+1678
-1901
File diff suppressed because it is too large
Load Diff
+4
-3
@@ -1,12 +1,12 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "yargi-mcp"
|
name = "yargi-mcp"
|
||||||
version = "0.1.3"
|
version = "0.1.6"
|
||||||
description = "MCP Server For Turkish Legal Databases"
|
description = "MCP Server For Turkish Legal Databases"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
license = {text = "MIT"}
|
license = {text = "MIT"}
|
||||||
authors = [{name = "Said Surucu", email = "saidsrc@gmail.com"}]
|
authors = [{name = "Said Surucu", email = "saidsrc@gmail.com"}]
|
||||||
keywords = ["mcp", "turkish-law", "legal", "yargitay", "danistay", "turkish", "law", "court", "decisions"]
|
keywords = ["mcp", "turkish-law", "legal", "yargitay", "danistay", "bddk", "kvkk", "turkish", "law", "court", "decisions"]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 4 - Beta",
|
"Development Status :: 4 - Beta",
|
||||||
"Intended Audience :: Legal Industry",
|
"Intended Audience :: Legal Industry",
|
||||||
@@ -26,10 +26,11 @@ dependencies = [
|
|||||||
"pydantic>=2.11.4",
|
"pydantic>=2.11.4",
|
||||||
"aiohttp>=3.11.18",
|
"aiohttp>=3.11.18",
|
||||||
"playwright>=1.52.0",
|
"playwright>=1.52.0",
|
||||||
"fastmcp>=2.10.3",
|
"fastmcp>=2.10.5",
|
||||||
"pypdf>=5.5.0",
|
"pypdf>=5.5.0",
|
||||||
"fastapi>=0.115.14",
|
"fastapi>=0.115.14",
|
||||||
"PyJWT>=2.8.0",
|
"PyJWT>=2.8.0",
|
||||||
|
"tiktoken>=0.5.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
|||||||
@@ -25,35 +25,31 @@ class RekabetKararTuruAdiEnum(str, Enum):
|
|||||||
|
|
||||||
class RekabetKurumuSearchRequest(BaseModel):
|
class RekabetKurumuSearchRequest(BaseModel):
|
||||||
"""Model for Rekabet Kurumu (Turkish Competition Authority) search request."""
|
"""Model for Rekabet Kurumu (Turkish Competition Authority) search request."""
|
||||||
sayfaAdi: Optional[str] = Field(None, description="Search in decision title (Başlık).")
|
sayfaAdi: str = Field("", description="Title")
|
||||||
YayinlanmaTarihi: Optional[str] = Field(None, description="Publication date (Yayım Tarihi), e.g., DD.MM.YYYY.")
|
YayinlanmaTarihi: str = Field("", description="Date")
|
||||||
PdfText: Optional[str] = Field(
|
PdfText: str = Field("", description="Text")
|
||||||
None,
|
KararTuruID: RekabetKararTuruGuidEnum = Field(RekabetKararTuruGuidEnum.TUMU, description="Type")
|
||||||
description='Search in decision text (Metin). For an exact phrase match, enclose the phrase in double quotes (e.g., "\\"vertical agreement\\" competition). The website indicates that using "" provides more precise results for phrases.'
|
KararSayisi: str = Field("", description="No")
|
||||||
)
|
KararTarihi: str = Field("", description="Date")
|
||||||
# This field uses the GUID enum as it's used by the client to make the actual web request.
|
page: int = Field(1, ge=1, description="Page")
|
||||||
KararTuruID: Optional[RekabetKararTuruGuidEnum] = Field(RekabetKararTuruGuidEnum.TUMU, description="Decision type (Karar Türü) GUID for internal client use, corresponding to the website's values.")
|
|
||||||
KararSayisi: Optional[str] = Field(None, description="Decision number (Karar Sayısı).")
|
|
||||||
KararTarihi: Optional[str] = Field(None, description="Decision date (Karar Tarihi), e.g., DD.MM.YYYY.")
|
|
||||||
page: int = Field(1, ge=1, description="Page number to fetch for results list.")
|
|
||||||
|
|
||||||
class RekabetDecisionSummary(BaseModel):
|
class RekabetDecisionSummary(BaseModel):
|
||||||
"""Model for a single Rekabet Kurumu decision summary from search results."""
|
"""Model for a single Rekabet Kurumu decision summary from search results."""
|
||||||
publication_date: Optional[str] = Field(None, description="Publication Date (Yayımlanma Tarihi).")
|
publication_date: str = Field("", description="Pub date")
|
||||||
decision_number: Optional[str] = Field(None, description="Decision Number (Karar Sayısı).")
|
decision_number: str = Field("", description="Number")
|
||||||
decision_date: Optional[str] = Field(None, description="Decision Date (Karar Tarihi).")
|
decision_date: str = Field("", description="Date")
|
||||||
decision_type_text: Optional[str] = Field(None, description="Decision Type as text (Karar Türü - metin olarak).")
|
decision_type_text: str = Field("", description="Type")
|
||||||
title: Optional[str] = Field(None, description="Decision title or summary text.")
|
title: str = Field("", description="Title")
|
||||||
decision_url: Optional[HttpUrl] = Field(None, description="URL to the decision's landing page (e.g., /Karar?kararId=...).")
|
decision_url: str = Field("", description="URL")
|
||||||
karar_id: Optional[str] = Field(None, description="GUID of the decision, extracted from its URL.")
|
karar_id: str = Field("", description="ID")
|
||||||
related_cases_url: Optional[HttpUrl] = Field(None, description="URL to related court cases page, if available.")
|
related_cases_url: str = Field("", description="Cases URL")
|
||||||
|
|
||||||
class RekabetSearchResult(BaseModel):
|
class RekabetSearchResult(BaseModel):
|
||||||
"""Model for the overall search result for Rekabet Kurumu decisions."""
|
"""Model for the overall search result for Rekabet Kurumu decisions."""
|
||||||
decisions: List[RekabetDecisionSummary]
|
decisions: List[RekabetDecisionSummary]
|
||||||
total_records_found: Optional[int] = Field(None, description="Total number of records found matching the query.")
|
total_records_found: int = Field(0, description="Total")
|
||||||
retrieved_page_number: int = Field(description="The page number of the results that were retrieved.")
|
retrieved_page_number: int = Field(description="Page")
|
||||||
total_pages: Optional[int] = Field(None, description="Total number of pages available for the query.")
|
total_pages: int = Field(0, description="Pages")
|
||||||
|
|
||||||
class RekabetDocument(BaseModel):
|
class RekabetDocument(BaseModel):
|
||||||
"""
|
"""
|
||||||
@@ -61,16 +57,15 @@ class RekabetDocument(BaseModel):
|
|||||||
Contains metadata from the landing page, a link to the PDF,
|
Contains metadata from the landing page, a link to the PDF,
|
||||||
and the PDF's content converted to paginated Markdown.
|
and the PDF's content converted to paginated Markdown.
|
||||||
"""
|
"""
|
||||||
source_landing_page_url: HttpUrl = Field(description="The URL of the decision's landing page from which the PDF was identified.")
|
source_landing_page_url: HttpUrl = Field(description="Source URL")
|
||||||
karar_id: str = Field(description="GUID of the decision.")
|
karar_id: str = Field(description="ID")
|
||||||
|
|
||||||
title_on_landing_page: Optional[str] = Field(None, description="Title as found on the landing page (e.g., from <title> tag or a main heading). Could be a generic title if direct PDF.")
|
title_on_landing_page: Optional[str] = Field(None, description="Title")
|
||||||
pdf_url: Optional[HttpUrl] = Field(None, description="Direct URL to the decision PDF document, if successfully found and resolved.")
|
pdf_url: Optional[HttpUrl] = Field(None, description="PDF URL")
|
||||||
|
|
||||||
# Fields for Markdown content derived from the PDF
|
markdown_chunk: Optional[str] = Field(None, description="Content")
|
||||||
markdown_chunk: Optional[str] = Field(None, description="A 5,000 character chunk of the Markdown content derived from the decision PDF.")
|
current_page: int = Field(1, description="Page")
|
||||||
current_page: int = Field(1, description="The current page number of the PDF-derived markdown chunk (1-indexed).")
|
total_pages: int = Field(1, description="Total pages")
|
||||||
total_pages: int = Field(1, description="Total number of pages for the full PDF-derived markdown content. Will be 0 if content could not be processed.")
|
is_paginated: bool = Field(False, description="Paginated")
|
||||||
is_paginated: bool = Field(False, description="True if the full PDF-derived markdown content is split into multiple pages.")
|
|
||||||
|
|
||||||
error_message: Optional[str] = Field(None, description="Contains an error message if the document retrieval or processing failed at any stage.")
|
error_message: Optional[str] = Field(None, description="Error")
|
||||||
@@ -16,7 +16,7 @@ from .models import (
|
|||||||
DaireSearchRequest, DaireSearchResponse, DaireDecision,
|
DaireSearchRequest, DaireSearchResponse, DaireDecision,
|
||||||
SayistayDocumentMarkdown
|
SayistayDocumentMarkdown
|
||||||
)
|
)
|
||||||
from .enums import DaireEnum, KamuIdaresiTuruEnum, WebKararKonusuEnum
|
from .enums import DaireEnum, KamuIdaresiTuruEnum, WebKararKonusuEnum, WEB_KARAR_KONUSU_MAPPING
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
if not logger.hasHandlers():
|
if not logger.hasHandlers():
|
||||||
@@ -134,6 +134,11 @@ class SayistayApiClient:
|
|||||||
return "Tüm Kurumlar"
|
return "Tüm Kurumlar"
|
||||||
elif enum_type == "web_karar_konusu":
|
elif enum_type == "web_karar_konusu":
|
||||||
return "Tüm Konular"
|
return "Tüm Konular"
|
||||||
|
|
||||||
|
# Apply web_karar_konusu mapping
|
||||||
|
if enum_type == "web_karar_konusu":
|
||||||
|
return WEB_KARAR_KONUSU_MAPPING.get(enum_value, enum_value)
|
||||||
|
|
||||||
return enum_value
|
return enum_value
|
||||||
|
|
||||||
def _build_datatables_params(self, start: int, length: int, draw: int = 1) -> List[Tuple[str, str]]:
|
def _build_datatables_params(self, start: int, length: int, draw: int = 1) -> List[Tuple[str, str]]:
|
||||||
|
|||||||
@@ -28,18 +28,30 @@ KamuIdaresiTuruEnum = Literal[
|
|||||||
"Diğer" # Other
|
"Diğer" # Other
|
||||||
]
|
]
|
||||||
|
|
||||||
# Decision Subject Categories (Web Karar Konusu)
|
# Decision Subject Categories (Web Karar Konusu) - Shortened for token efficiency
|
||||||
WebKararKonusuEnum = Literal[
|
WebKararKonusuEnum = Literal[
|
||||||
"ALL", # All subjects
|
"ALL", # All subjects
|
||||||
"Harcırah Mevzuatı ile İlgili Kararlar", # Travel Allowance Legislation Related Decisions
|
"Harcırah Mevzuatı", # Travel Allowance Legislation
|
||||||
"İhale Mevzuatı ile İlgili Kararlar", # Procurement Legislation Related Decisions
|
"İhale Mevzuatı", # Procurement Legislation
|
||||||
"İş Mevzuatı ile İlgili Kararlar", # Labor Legislation Related Decisions
|
"İş Mevzuatı", # Labor Legislation
|
||||||
"Personel Mevzuatı ile İlgili Kararlar", # Personnel Legislation Related Decisions
|
"Personel Mevzuatı", # Personnel Legislation
|
||||||
"Sorumluluk ve Yargılama Usulleri ile İlgili Kararlar", # Liability and Trial Procedures Related Decisions
|
"Sorumluluk ve Yargılama Usulleri", # Liability and Trial Procedures
|
||||||
"Vergi Resmi Harç ve Diğer Gelirlerle İlgili Kararlar", # Tax, Official Fee and Other Revenue Related Decisions
|
"Vergi Resmi Harç ve Diğer Gelirler", # Tax, Official Fee and Other Revenue
|
||||||
"Çeşitli Konuları İlgilendiren Kararlar" # Decisions Concerning Various Topics
|
"Çeşitli Konular" # Various Topics
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Mapping from shortened enum values to full API values
|
||||||
|
WEB_KARAR_KONUSU_MAPPING = {
|
||||||
|
"ALL": "ALL",
|
||||||
|
"Harcırah Mevzuatı": "Harcırah Mevzuatı ile İlgili Kararlar",
|
||||||
|
"İhale Mevzuatı": "İhale Mevzuatı ile İlgili Kararlar",
|
||||||
|
"İş Mevzuatı": "İş Mevzuatı ile İlgili Kararlar",
|
||||||
|
"Personel Mevzuatı": "Personel Mevzuatı ile İlgili Kararlar",
|
||||||
|
"Sorumluluk ve Yargılama Usulleri": "Sorumluluk ve Yargılama Usulleri ile İlgili Kararlar",
|
||||||
|
"Vergi Resmi Harç ve Diğer Gelirler": "Vergi Resmi Harç ve Diğer Gelirlerle İlgili Kararlar",
|
||||||
|
"Çeşitli Konular": "Çeşitli Konuları İlgilendiren Kararlar"
|
||||||
|
}
|
||||||
|
|
||||||
# Year ranges for different endpoints
|
# Year ranges for different endpoints
|
||||||
GENEL_KURUL_YEARS = [str(year) for year in range(2006, 2025)] # 2006-2024
|
GENEL_KURUL_YEARS = [str(year) for year in range(2006, 2025)] # 2006-2024
|
||||||
TEMYIZ_KURULU_YEARS = [str(year) for year in range(1993, 2023)] # 1993-2022
|
TEMYIZ_KURULU_YEARS = [str(year) for year in range(1993, 2023)] # 1993-2022
|
||||||
|
|||||||
+90
-112
@@ -1,9 +1,16 @@
|
|||||||
# sayistay_mcp_module/models.py
|
# sayistay_mcp_module/models.py
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from typing import Optional, List, Union
|
from typing import Optional, List, Union, Dict, Any, Literal
|
||||||
|
from enum import Enum
|
||||||
from .enums import DaireEnum, KamuIdaresiTuruEnum, WebKararKonusuEnum
|
from .enums import DaireEnum, KamuIdaresiTuruEnum, WebKararKonusuEnum
|
||||||
|
|
||||||
|
# --- Unified Enums ---
|
||||||
|
class SayistayDecisionTypeEnum(str, Enum):
|
||||||
|
GENEL_KURUL = "genel_kurul"
|
||||||
|
TEMYIZ_KURULU = "temyiz_kurulu"
|
||||||
|
DAIRE = "daire"
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Genel Kurul (General Assembly) Models
|
# Genel Kurul (General Assembly) Models
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -16,30 +23,18 @@ class GenelKurulSearchRequest(BaseModel):
|
|||||||
of the Turkish Court of Accounts, typically addressing interpretation of
|
of the Turkish Court of Accounts, typically addressing interpretation of
|
||||||
audit and accountability regulations.
|
audit and accountability regulations.
|
||||||
"""
|
"""
|
||||||
karar_no: Optional[str] = Field(None, description="Decision number (e.g., '5415')")
|
karar_no: str = Field("", description="Decision no")
|
||||||
karar_ek: Optional[str] = Field(None, description="Decision appendix number (max 99)")
|
karar_ek: str = Field("", description="Appendix no")
|
||||||
|
|
||||||
karar_tarih_baslangic: Optional[str] = Field(None, description="""
|
karar_tarih_baslangic: str = Field("", description="Start year (YYYY)")
|
||||||
Decision start year for date range filtering.
|
|
||||||
Available years: 2006-2024. Format: 'YYYY' (e.g., '2020')
|
|
||||||
Use with karar_tarih_bitis for date range filtering.
|
|
||||||
""")
|
|
||||||
|
|
||||||
karar_tarih_bitis: Optional[str] = Field(None, description="""
|
karar_tarih_bitis: str = Field("", description="End year")
|
||||||
Decision end year for date range filtering.
|
|
||||||
Available years: 2006-2024. Format: 'YYYY' (e.g., '2024')
|
|
||||||
Use with karar_tarih_baslangic for date range filtering.
|
|
||||||
""")
|
|
||||||
|
|
||||||
karar_tamami: Optional[str] = Field(None, description="""
|
karar_tamami: str = Field("", description="Value")
|
||||||
Content/text search within decision summaries (max 400 characters).
|
|
||||||
Searches in decision abstracts and main content.
|
|
||||||
Example: 'belediye taşınmaz tahsis'
|
|
||||||
""")
|
|
||||||
|
|
||||||
# DataTables pagination
|
# DataTables pagination
|
||||||
start: int = Field(0, description="Starting record for pagination (0-based)")
|
start: int = Field(0, description="Starting record for pagination (0-based)")
|
||||||
length: int = Field(10, description="Number of records per page (1-100)")
|
length: int = Field(10, description="Number of records per page (1-10)")
|
||||||
|
|
||||||
class GenelKurulDecision(BaseModel):
|
class GenelKurulDecision(BaseModel):
|
||||||
"""Single Genel Kurul decision entry from search results."""
|
"""Single Genel Kurul decision entry from search results."""
|
||||||
@@ -66,62 +61,27 @@ class TemyizKuruluSearchRequest(BaseModel):
|
|||||||
Temyiz Kurulu reviews appeals against audit chamber decisions,
|
Temyiz Kurulu reviews appeals against audit chamber decisions,
|
||||||
providing higher-level review of audit findings and sanctions.
|
providing higher-level review of audit findings and sanctions.
|
||||||
"""
|
"""
|
||||||
ilam_dairesi: DaireEnum = Field("ALL", description="""
|
ilam_dairesi: DaireEnum = Field("ALL", description="Value")
|
||||||
Chamber/Department filter for appeals board decisions.
|
|
||||||
• ALL: All chambers (default)
|
|
||||||
• 1-8: Specific chamber number (1. Daire through 8. Daire)
|
|
||||||
Each chamber specializes in different types of public institutions.
|
|
||||||
""")
|
|
||||||
|
|
||||||
yili: Optional[str] = Field(None, description="""
|
yili: str = Field("", description="Value")
|
||||||
Account year filter (Hesap Yılı).
|
|
||||||
Available years: 1993-2022. Format: 'YYYY' (e.g., '2020')
|
|
||||||
Refers to the fiscal year being audited, not decision date.
|
|
||||||
""")
|
|
||||||
|
|
||||||
karar_tarih_baslangic: Optional[str] = Field(None, description="""
|
karar_tarih_baslangic: str = Field("", description="Value")
|
||||||
Decision start year for date range filtering.
|
|
||||||
Available years: 2000, 2006-2024. Format: 'YYYY' (e.g., '2020')
|
|
||||||
Use with karar_tarih_bitis for date range filtering.
|
|
||||||
""")
|
|
||||||
|
|
||||||
karar_tarih_bitis: Optional[str] = Field(None, description="""
|
karar_tarih_bitis: str = Field("", description="End year")
|
||||||
Decision end year for date range filtering.
|
|
||||||
Available years: 2000, 2006-2024. Format: 'YYYY' (e.g., '2024')
|
|
||||||
Use with karar_tarih_baslangic for date range filtering.
|
|
||||||
""")
|
|
||||||
|
|
||||||
kamu_idaresi_turu: KamuIdaresiTuruEnum = Field("ALL", description="""
|
kamu_idaresi_turu: KamuIdaresiTuruEnum = Field("ALL", description="Value")
|
||||||
Public administration type filter:
|
|
||||||
• ALL: All institutions (default)
|
|
||||||
• Genel Bütçe Kapsamındaki İdareler: General budget administrations
|
|
||||||
• Yüksek Öğretim Kurumları: Higher education institutions
|
|
||||||
• Belediyeler ve Bağlı İdareler: Municipalities and affiliates
|
|
||||||
• Other specific institution types
|
|
||||||
""")
|
|
||||||
|
|
||||||
ilam_no: Optional[str] = Field(None, description="Audit report number (İlam No, max 50 chars)")
|
ilam_no: str = Field("", description="Audit report number (İlam No, max 50 chars)")
|
||||||
dosya_no: Optional[str] = Field(None, description="File number for the case")
|
dosya_no: str = Field("", description="File number for the case")
|
||||||
temyiz_tutanak_no: Optional[str] = Field(None, description="Appeals board meeting minutes number")
|
temyiz_tutanak_no: str = Field("", description="Appeals board meeting minutes number")
|
||||||
|
|
||||||
temyiz_karar: Optional[str] = Field(None, description="""
|
temyiz_karar: str = Field("", description="Value")
|
||||||
Content search within appeals decisions.
|
|
||||||
Searches decision text and reasoning.
|
|
||||||
Example: 'araç kiralama kasko'
|
|
||||||
""")
|
|
||||||
|
|
||||||
web_karar_konusu: WebKararKonusuEnum = Field("ALL", description="""
|
web_karar_konusu: WebKararKonusuEnum = Field("ALL", description="Value")
|
||||||
Decision subject category filter:
|
|
||||||
• ALL: All subjects (default)
|
|
||||||
• İhale Mevzuatı ile İlgili Kararlar: Procurement legislation
|
|
||||||
• Personel Mevzuatı ile İlgili Kararlar: Personnel legislation
|
|
||||||
• Harcırah Mevzuatı ile İlgili Kararlar: Travel allowance legislation
|
|
||||||
• Other specialized legal areas
|
|
||||||
""")
|
|
||||||
|
|
||||||
# DataTables pagination
|
# DataTables pagination
|
||||||
start: int = Field(0, description="Starting record for pagination (0-based)")
|
start: int = Field(0, description="Starting record for pagination (0-based)")
|
||||||
length: int = Field(10, description="Number of records per page (1-100)")
|
length: int = Field(10, description="Number of records per page (1-10)")
|
||||||
|
|
||||||
class TemyizKuruluDecision(BaseModel):
|
class TemyizKuruluDecision(BaseModel):
|
||||||
"""Single Temyiz Kurulu decision entry from search results."""
|
"""Single Temyiz Kurulu decision entry from search results."""
|
||||||
@@ -148,60 +108,25 @@ class DaireSearchRequest(BaseModel):
|
|||||||
Daire decisions are first-instance audit findings and sanctions
|
Daire decisions are first-instance audit findings and sanctions
|
||||||
issued by individual audit chambers before potential appeals.
|
issued by individual audit chambers before potential appeals.
|
||||||
"""
|
"""
|
||||||
yargilama_dairesi: DaireEnum = Field("ALL", description="""
|
yargilama_dairesi: DaireEnum = Field("ALL", description="Value")
|
||||||
Audit chamber filter:
|
|
||||||
• ALL: All chambers (default)
|
|
||||||
• 1-8: Specific chamber number (1. Daire through 8. Daire)
|
|
||||||
Each chamber audits different types of public institutions.
|
|
||||||
""")
|
|
||||||
|
|
||||||
karar_tarih_baslangic: Optional[str] = Field(None, description="""
|
karar_tarih_baslangic: str = Field("", description="Value")
|
||||||
Decision start year for date range filtering.
|
|
||||||
Available years: 2012-2025. Format: 'YYYY' (e.g., '2020')
|
|
||||||
Use with karar_tarih_bitis for date range filtering.
|
|
||||||
""")
|
|
||||||
|
|
||||||
karar_tarih_bitis: Optional[str] = Field(None, description="""
|
karar_tarih_bitis: str = Field("", description="End year")
|
||||||
Decision end year for date range filtering.
|
|
||||||
Available years: 2012-2025. Format: 'YYYY' (e.g., '2024')
|
|
||||||
Use with karar_tarih_baslangic for date range filtering.
|
|
||||||
""")
|
|
||||||
|
|
||||||
ilam_no: Optional[str] = Field(None, description="Audit report number (İlam No, max 50 chars)")
|
ilam_no: str = Field("", description="Audit report number (İlam No, max 50 chars)")
|
||||||
|
|
||||||
kamu_idaresi_turu: KamuIdaresiTuruEnum = Field("ALL", description="""
|
kamu_idaresi_turu: KamuIdaresiTuruEnum = Field("ALL", description="Value")
|
||||||
Public administration type filter:
|
|
||||||
• ALL: All institutions (default)
|
|
||||||
• Genel Bütçe Kapsamındaki İdareler: General budget administrations
|
|
||||||
• Yüksek Öğretim Kurumları: Higher education institutions
|
|
||||||
• Belediyeler ve Bağlı İdareler: Municipalities and affiliates
|
|
||||||
• Other specific institution types
|
|
||||||
""")
|
|
||||||
|
|
||||||
hesap_yili: Optional[str] = Field(None, description="""
|
hesap_yili: str = Field("", description="Value")
|
||||||
Account year filter (Hesap Yılı).
|
|
||||||
Available years: 2005, 2008-2023. Format: 'YYYY' (e.g., '2020')
|
|
||||||
Refers to the fiscal year being audited, not decision date.
|
|
||||||
""")
|
|
||||||
|
|
||||||
web_karar_konusu: WebKararKonusuEnum = Field("ALL", description="""
|
web_karar_konusu: WebKararKonusuEnum = Field("ALL", description="Value")
|
||||||
Decision subject category filter:
|
|
||||||
• ALL: All subjects (default)
|
|
||||||
• İhale Mevzuatı ile İlgili Kararlar: Procurement legislation
|
|
||||||
• Personel Mevzuatı ile İlgili Kararlar: Personnel legislation
|
|
||||||
• Vergi Resmi Harç ve Diğer Gelirlerle İlgili Kararlar: Tax and fee legislation
|
|
||||||
• Other specialized legal areas
|
|
||||||
""")
|
|
||||||
|
|
||||||
web_karar_metni: Optional[str] = Field(None, description="""
|
web_karar_metni: str = Field("", description="Value")
|
||||||
Content search within chamber decisions.
|
|
||||||
Searches decision text and audit findings.
|
|
||||||
Example: 'birim fiyat revize edilmemesi'
|
|
||||||
""")
|
|
||||||
|
|
||||||
# DataTables pagination
|
# DataTables pagination
|
||||||
start: int = Field(0, description="Starting record for pagination (0-based)")
|
start: int = Field(0, description="Starting record for pagination (0-based)")
|
||||||
length: int = Field(10, description="Number of records per page (1-100)")
|
length: int = Field(10, description="Number of records per page (1-10)")
|
||||||
|
|
||||||
class DaireDecision(BaseModel):
|
class DaireDecision(BaseModel):
|
||||||
"""Single Daire decision entry from search results."""
|
"""Single Daire decision entry from search results."""
|
||||||
@@ -209,7 +134,7 @@ class DaireDecision(BaseModel):
|
|||||||
yargilama_dairesi: int = Field(..., description="Chamber number (1-8)")
|
yargilama_dairesi: int = Field(..., description="Chamber number (1-8)")
|
||||||
karar_tarih: str = Field(..., description="Decision date in DD.MM.YYYY format")
|
karar_tarih: str = Field(..., description="Decision date in DD.MM.YYYY format")
|
||||||
karar_no: str = Field(..., description="Decision number")
|
karar_no: str = Field(..., description="Decision number")
|
||||||
ilam_no: Optional[str] = Field(None, description="Audit report number (may be null)")
|
ilam_no: str = Field("", description="Audit report number (may be null)")
|
||||||
madde_no: int = Field(..., description="Article/item number within the decision")
|
madde_no: int = Field(..., description="Article/item number within the decision")
|
||||||
kamu_idaresi_turu: str = Field(..., description="Public administration type")
|
kamu_idaresi_turu: str = Field(..., description="Public administration type")
|
||||||
hesap_yili: int = Field(..., description="Account year being audited")
|
hesap_yili: int = Field(..., description="Account year being audited")
|
||||||
@@ -235,8 +160,61 @@ class SayistayDocumentMarkdown(BaseModel):
|
|||||||
decision types (Genel Kurul, Temyiz Kurulu, Daire).
|
decision types (Genel Kurul, Temyiz Kurulu, Daire).
|
||||||
"""
|
"""
|
||||||
decision_id: str = Field(..., description="Unique decision identifier")
|
decision_id: str = Field(..., description="Unique decision identifier")
|
||||||
decision_type: str = Field(..., description="Type of decision: 'genel_kurul', 'temyiz_kurulu', or 'daire'")
|
decision_type: str = Field(..., description="Value")
|
||||||
source_url: str = Field(..., description="Original URL where the document was retrieved")
|
source_url: str = Field(..., description="Original URL where the document was retrieved")
|
||||||
markdown_content: Optional[str] = Field(None, description="Full decision text converted to Markdown format")
|
markdown_content: Optional[str] = Field(None, description="Full decision text converted to Markdown format")
|
||||||
retrieval_date: Optional[str] = Field(None, description="Date when document was retrieved (ISO format)")
|
retrieval_date: Optional[str] = Field(None, description="Date when document was retrieved (ISO format)")
|
||||||
error_message: Optional[str] = Field(None, description="Error message if document retrieval failed")
|
error_message: Optional[str] = Field(None, description="Error message if document retrieval failed")
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Unified Models
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
class SayistayUnifiedSearchRequest(BaseModel):
|
||||||
|
"""Unified search request for all Sayıştay decision types."""
|
||||||
|
decision_type: Literal["genel_kurul", "temyiz_kurulu", "daire"] = Field(..., description="Decision type: genel_kurul, temyiz_kurulu, or daire")
|
||||||
|
|
||||||
|
# Common pagination parameters
|
||||||
|
start: int = Field(0, ge=0, description="Starting record for pagination (0-based)")
|
||||||
|
length: int = Field(10, ge=1, le=100, description="Number of records per page (1-100)")
|
||||||
|
|
||||||
|
# Common search parameters
|
||||||
|
karar_tarih_baslangic: str = Field("", description="Start date (DD.MM.YYYY format)")
|
||||||
|
karar_tarih_bitis: str = Field("", description="End date (DD.MM.YYYY format)")
|
||||||
|
kamu_idaresi_turu: KamuIdaresiTuruEnum = Field("ALL", description="Public administration type filter")
|
||||||
|
ilam_no: str = Field("", description="Audit report number (İlam No, max 50 chars)")
|
||||||
|
web_karar_konusu: WebKararKonusuEnum = Field("ALL", description="Decision subject category filter")
|
||||||
|
|
||||||
|
# Genel Kurul specific parameters (ignored for other types)
|
||||||
|
karar_no: str = Field("", description="Decision number (genel_kurul only)")
|
||||||
|
karar_ek: str = Field("", description="Decision appendix number (genel_kurul only)")
|
||||||
|
karar_tamami: str = Field("", description="Full text search (genel_kurul only)")
|
||||||
|
|
||||||
|
# Temyiz Kurulu specific parameters (ignored for other types)
|
||||||
|
ilam_dairesi: DaireEnum = Field("ALL", description="Audit chamber selection (temyiz_kurulu only)")
|
||||||
|
yili: str = Field("", description="Year (YYYY format, temyiz_kurulu only)")
|
||||||
|
dosya_no: str = Field("", description="File number (temyiz_kurulu only)")
|
||||||
|
temyiz_tutanak_no: str = Field("", description="Appeals board meeting minutes number (temyiz_kurulu only)")
|
||||||
|
temyiz_karar: str = Field("", description="Appeals decision text search (temyiz_kurulu only)")
|
||||||
|
|
||||||
|
# Daire specific parameters (ignored for other types)
|
||||||
|
yargilama_dairesi: DaireEnum = Field("ALL", description="Chamber selection (daire only)")
|
||||||
|
hesap_yili: str = Field("", description="Account year (daire only)")
|
||||||
|
web_karar_metni: str = Field("", description="Decision text search (daire only)")
|
||||||
|
|
||||||
|
class SayistayUnifiedSearchResult(BaseModel):
|
||||||
|
"""Unified search result containing decisions from any Sayıştay decision type."""
|
||||||
|
decision_type: Literal["genel_kurul", "temyiz_kurulu", "daire"] = Field(..., description="Type of decisions returned")
|
||||||
|
decisions: List[Dict[str, Any]] = Field(default_factory=list, description="Decision list (structure varies by type)")
|
||||||
|
total_records: int = Field(0, description="Total number of records found")
|
||||||
|
total_filtered: int = Field(0, description="Number of records after filtering")
|
||||||
|
draw: int = Field(1, description="DataTables draw counter")
|
||||||
|
|
||||||
|
class SayistayUnifiedDocumentMarkdown(BaseModel):
|
||||||
|
"""Unified document model for all Sayıştay decision types."""
|
||||||
|
decision_type: Literal["genel_kurul", "temyiz_kurulu", "daire"] = Field(..., description="Type of document")
|
||||||
|
decision_id: str = Field(..., description="Decision ID")
|
||||||
|
source_url: str = Field(..., description="Source URL of the document")
|
||||||
|
document_data: Dict[str, Any] = Field(default_factory=dict, description="Document content and metadata")
|
||||||
|
markdown_content: Optional[str] = Field(None, description="Markdown content")
|
||||||
|
error_message: Optional[str] = Field(None, description="Error message if retrieval failed")
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
# sayistay_mcp_module/unified_client.py
|
||||||
|
# Unified client for all three Sayıştay decision types
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from .models import (
|
||||||
|
SayistayUnifiedSearchRequest,
|
||||||
|
SayistayUnifiedSearchResult,
|
||||||
|
SayistayUnifiedDocumentMarkdown,
|
||||||
|
GenelKurulSearchRequest,
|
||||||
|
TemyizKuruluSearchRequest,
|
||||||
|
DaireSearchRequest
|
||||||
|
)
|
||||||
|
from .client import SayistayApiClient
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class SayistayUnifiedClient:
|
||||||
|
"""Unified client that handles all three Sayıştay decision types."""
|
||||||
|
|
||||||
|
def __init__(self, request_timeout: float = 60.0):
|
||||||
|
self.client = SayistayApiClient(request_timeout)
|
||||||
|
|
||||||
|
async def search_unified(self, params: SayistayUnifiedSearchRequest) -> SayistayUnifiedSearchResult:
|
||||||
|
"""Unified search that routes to appropriate search method based on decision_type."""
|
||||||
|
|
||||||
|
if params.decision_type == "genel_kurul":
|
||||||
|
# Convert to genel kurul request
|
||||||
|
genel_kurul_params = GenelKurulSearchRequest(
|
||||||
|
karar_no=params.karar_no,
|
||||||
|
karar_ek=params.karar_ek,
|
||||||
|
karar_tarih_baslangic=params.karar_tarih_baslangic,
|
||||||
|
karar_tarih_bitis=params.karar_tarih_bitis,
|
||||||
|
karar_tamami=params.karar_tamami,
|
||||||
|
start=params.start,
|
||||||
|
length=params.length
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await self.client.search_genel_kurul_decisions(genel_kurul_params)
|
||||||
|
|
||||||
|
# Convert to unified format
|
||||||
|
decisions_list = [decision.model_dump() for decision in result.decisions]
|
||||||
|
|
||||||
|
return SayistayUnifiedSearchResult(
|
||||||
|
decision_type="genel_kurul",
|
||||||
|
decisions=decisions_list,
|
||||||
|
total_records=result.total_records,
|
||||||
|
total_filtered=result.total_filtered,
|
||||||
|
draw=result.draw
|
||||||
|
)
|
||||||
|
|
||||||
|
elif params.decision_type == "temyiz_kurulu":
|
||||||
|
# Convert to temyiz kurulu request
|
||||||
|
temyiz_params = TemyizKuruluSearchRequest(
|
||||||
|
ilam_dairesi=params.ilam_dairesi,
|
||||||
|
yili=params.yili,
|
||||||
|
karar_tarih_baslangic=params.karar_tarih_baslangic,
|
||||||
|
karar_tarih_bitis=params.karar_tarih_bitis,
|
||||||
|
kamu_idaresi_turu=params.kamu_idaresi_turu,
|
||||||
|
ilam_no=params.ilam_no,
|
||||||
|
dosya_no=params.dosya_no,
|
||||||
|
temyiz_tutanak_no=params.temyiz_tutanak_no,
|
||||||
|
temyiz_karar=params.temyiz_karar,
|
||||||
|
web_karar_konusu=params.web_karar_konusu,
|
||||||
|
start=params.start,
|
||||||
|
length=params.length
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await self.client.search_temyiz_kurulu_decisions(temyiz_params)
|
||||||
|
|
||||||
|
# Convert to unified format
|
||||||
|
decisions_list = [decision.model_dump() for decision in result.decisions]
|
||||||
|
|
||||||
|
return SayistayUnifiedSearchResult(
|
||||||
|
decision_type="temyiz_kurulu",
|
||||||
|
decisions=decisions_list,
|
||||||
|
total_records=result.total_records,
|
||||||
|
total_filtered=result.total_filtered,
|
||||||
|
draw=result.draw
|
||||||
|
)
|
||||||
|
|
||||||
|
elif params.decision_type == "daire":
|
||||||
|
# Convert to daire request
|
||||||
|
daire_params = DaireSearchRequest(
|
||||||
|
yargilama_dairesi=params.yargilama_dairesi,
|
||||||
|
karar_tarih_baslangic=params.karar_tarih_baslangic,
|
||||||
|
karar_tarih_bitis=params.karar_tarih_bitis,
|
||||||
|
ilam_no=params.ilam_no,
|
||||||
|
kamu_idaresi_turu=params.kamu_idaresi_turu,
|
||||||
|
hesap_yili=params.hesap_yili,
|
||||||
|
web_karar_konusu=params.web_karar_konusu,
|
||||||
|
web_karar_metni=params.web_karar_metni,
|
||||||
|
start=params.start,
|
||||||
|
length=params.length
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await self.client.search_daire_decisions(daire_params)
|
||||||
|
|
||||||
|
# Convert to unified format
|
||||||
|
decisions_list = [decision.model_dump() for decision in result.decisions]
|
||||||
|
|
||||||
|
return SayistayUnifiedSearchResult(
|
||||||
|
decision_type="daire",
|
||||||
|
decisions=decisions_list,
|
||||||
|
total_records=result.total_records,
|
||||||
|
total_filtered=result.total_filtered,
|
||||||
|
draw=result.draw
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported decision type: {params.decision_type}")
|
||||||
|
|
||||||
|
async def get_document_unified(self, decision_id: str, decision_type: str) -> SayistayUnifiedDocumentMarkdown:
|
||||||
|
"""Unified document retrieval for all Sayıştay decision types."""
|
||||||
|
|
||||||
|
# Use existing client method (decision_type is already a string)
|
||||||
|
result = await self.client.get_document_as_markdown(decision_id, decision_type)
|
||||||
|
|
||||||
|
return SayistayUnifiedDocumentMarkdown(
|
||||||
|
decision_type=decision_type,
|
||||||
|
decision_id=result.decision_id,
|
||||||
|
source_url=result.source_url,
|
||||||
|
document_data=result.model_dump(),
|
||||||
|
markdown_content=result.markdown_content,
|
||||||
|
error_message=result.error_message
|
||||||
|
)
|
||||||
|
|
||||||
|
async def close_client_session(self):
|
||||||
|
"""Close the underlying client session."""
|
||||||
|
if hasattr(self.client, 'close_client_session'):
|
||||||
|
await self.client.close_client_session()
|
||||||
@@ -28,41 +28,41 @@ class UyusmazlikKararSonucuEnum(str, Enum): # Based on checkbox text in the form
|
|||||||
|
|
||||||
class UyusmazlikSearchRequest(BaseModel): # This is the model the MCP tool will accept
|
class UyusmazlikSearchRequest(BaseModel): # This is the model the MCP tool will accept
|
||||||
"""Model for Uyuşmazlık Mahkemesi search request using user-friendly terms."""
|
"""Model for Uyuşmazlık Mahkemesi search request using user-friendly terms."""
|
||||||
icerik: Optional[str] = Field("", description="Keyword or content for main text search (Icerik).")
|
icerik: Optional[str] = Field("", description="Search text")
|
||||||
|
|
||||||
bolum: Optional[UyusmazlikBolumEnum] = Field(
|
bolum: Optional[UyusmazlikBolumEnum] = Field(
|
||||||
UyusmazlikBolumEnum.TUMU,
|
UyusmazlikBolumEnum.TUMU,
|
||||||
description="Select the department (Bölüm)."
|
description="Department"
|
||||||
)
|
)
|
||||||
uyusmazlik_turu: Optional[UyusmazlikTuruEnum] = Field(
|
uyusmazlik_turu: Optional[UyusmazlikTuruEnum] = Field(
|
||||||
UyusmazlikTuruEnum.TUMU,
|
UyusmazlikTuruEnum.TUMU,
|
||||||
description="Select the type of dispute (Uyuşmazlık)."
|
description="Dispute type"
|
||||||
)
|
)
|
||||||
|
|
||||||
# User provides a list of user-friendly names for Karar Sonucu
|
# User provides a list of user-friendly names for Karar Sonucu
|
||||||
karar_sonuclari: Optional[List[UyusmazlikKararSonucuEnum]] = Field( # Changed to list of Enums
|
karar_sonuclari: Optional[List[UyusmazlikKararSonucuEnum]] = Field( # Changed to list of Enums
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
description="List of desired 'Karar Sonucu' types."
|
description="Decision types"
|
||||||
)
|
)
|
||||||
|
|
||||||
esas_yil: Optional[str] = Field("", description="Case year ('Esas Yılı').")
|
esas_yil: Optional[str] = Field("", description="Case year")
|
||||||
esas_sayisi: Optional[str] = Field("", description="Case number ('Esas Sayısı').")
|
esas_sayisi: Optional[str] = Field("", description="Case no")
|
||||||
karar_yil: Optional[str] = Field("", description="Decision year ('Karar Yılı').")
|
karar_yil: Optional[str] = Field("", description="Decision year")
|
||||||
karar_sayisi: Optional[str] = Field("", description="Decision number ('Karar Sayısı').")
|
karar_sayisi: Optional[str] = Field("", description="Decision no")
|
||||||
kanun_no: Optional[str] = Field("", description="Relevant Law Number ('KanunNo').")
|
kanun_no: Optional[str] = Field("", description="Law no")
|
||||||
|
|
||||||
karar_date_begin: Optional[str] = Field("", description="Decision start date (DD.MM.YYYY) ('KararDateBegin').")
|
karar_date_begin: Optional[str] = Field("", description="Start date (DD.MM.YYYY)")
|
||||||
karar_date_end: Optional[str] = Field("", description="Decision end date (DD.MM.YYYY) ('KararDateEnd').")
|
karar_date_end: Optional[str] = Field("", description="End date (DD.MM.YYYY)")
|
||||||
|
|
||||||
resmi_gazete_sayi: Optional[str] = Field("", description="Official Gazette number ('ResmiGazeteSayi').")
|
resmi_gazete_sayi: Optional[str] = Field("", description="Gazette no")
|
||||||
resmi_gazete_date: Optional[str] = Field("", description="Official Gazette date (DD.MM.YYYY) ('ResmiGazeteDate').")
|
resmi_gazete_date: Optional[str] = Field("", description="Gazette date (DD.MM.YYYY)")
|
||||||
|
|
||||||
# Detailed text search fields from the "icerikDetail" section of the form
|
# Detailed text search fields from the "icerikDetail" section of the form
|
||||||
tumce: Optional[str] = Field("", description="Exact phrase search ('Tumce').")
|
tumce: Optional[str] = Field("", description="Exact phrase")
|
||||||
wild_card: Optional[str] = Field("", description="Search for phrase and its inflections ('WildCard').") # Changed from WildCard for Pythonic name
|
wild_card: Optional[str] = Field("", description="Wildcard search")
|
||||||
hepsi: Optional[str] = Field("", description="Search for texts containing all specified words ('Hepsi').")
|
hepsi: Optional[str] = Field("", description="All words")
|
||||||
herhangi_birisi: Optional[str] = Field("", description="Search for texts containing any of the specified words ('Herhangibirisi').")
|
herhangi_birisi: Optional[str] = Field("", description="Any word")
|
||||||
not_hepsi: Optional[str] = Field("", description="Exclude texts containing these specified words ('NotHepsi').")
|
not_hepsi: Optional[str] = Field("", description="Exclude words")
|
||||||
|
|
||||||
class UyusmazlikApiDecisionEntry(BaseModel):
|
class UyusmazlikApiDecisionEntry(BaseModel):
|
||||||
"""Model for an individual decision entry parsed from Uyuşmazlık API's HTML search response."""
|
"""Model for an individual decision entry parsed from Uyuşmazlık API's HTML search response."""
|
||||||
@@ -71,9 +71,9 @@ class UyusmazlikApiDecisionEntry(BaseModel):
|
|||||||
bolum: Optional[str] = Field(None)
|
bolum: Optional[str] = Field(None)
|
||||||
uyusmazlik_konusu: Optional[str] = Field(None)
|
uyusmazlik_konusu: Optional[str] = Field(None)
|
||||||
karar_sonucu: Optional[str] = Field(None)
|
karar_sonucu: Optional[str] = Field(None)
|
||||||
popover_content: Optional[str] = Field(None, description="Summary/description from popover.")
|
popover_content: Optional[str] = Field(None, description="Summary")
|
||||||
document_url: HttpUrl # Full URL to the decision document HTML page
|
document_url: HttpUrl # Full URL to the decision document HTML page
|
||||||
pdf_url: Optional[HttpUrl] = Field(None, description="Direct URL to PDF if available.")
|
pdf_url: Optional[HttpUrl] = Field(None, description="PDF URL")
|
||||||
|
|
||||||
class UyusmazlikSearchResponse(BaseModel): # This is what the MCP tool will return
|
class UyusmazlikSearchResponse(BaseModel): # This is what the MCP tool will return
|
||||||
"""Response model for Uyuşmazlık Mahkemesi search results for the MCP tool."""
|
"""Response model for Uyuşmazlık Mahkemesi search results for the MCP tool."""
|
||||||
|
|||||||
@@ -65,6 +65,19 @@ class YargitayOfficialApiClient:
|
|||||||
response.raise_for_status() # Raise an exception for HTTP 4xx or 5xx status codes
|
response.raise_for_status() # Raise an exception for HTTP 4xx or 5xx status codes
|
||||||
response_json_data = response.json()
|
response_json_data = response.json()
|
||||||
|
|
||||||
|
logger.debug(f"YargitayOfficialApiClient: Raw API response: {response_json_data}")
|
||||||
|
|
||||||
|
# Handle None or empty data response from API
|
||||||
|
if response_json_data is None:
|
||||||
|
logger.warning("YargitayOfficialApiClient: API returned None response")
|
||||||
|
response_json_data = {"data": {"data": [], "recordsTotal": 0, "recordsFiltered": 0}}
|
||||||
|
elif not isinstance(response_json_data, dict):
|
||||||
|
logger.warning(f"YargitayOfficialApiClient: API returned unexpected response type: {type(response_json_data)}")
|
||||||
|
response_json_data = {"data": {"data": [], "recordsTotal": 0, "recordsFiltered": 0}}
|
||||||
|
elif response_json_data.get("data") is None:
|
||||||
|
logger.warning("YargitayOfficialApiClient: API response data field is None")
|
||||||
|
response_json_data["data"] = {"data": [], "recordsTotal": 0, "recordsFiltered": 0}
|
||||||
|
|
||||||
# Validate and parse the response using Pydantic models
|
# Validate and parse the response using Pydantic models
|
||||||
api_response = YargitayApiSearchResponse(**response_json_data)
|
api_response = YargitayApiSearchResponse(**response_json_data)
|
||||||
|
|
||||||
|
|||||||
@@ -34,111 +34,70 @@ class YargitayDetailedSearchRequest(BaseModel):
|
|||||||
to Yargitay's detailed search endpoint (e.g., /aramadetaylist).
|
to Yargitay's detailed search endpoint (e.g., /aramadetaylist).
|
||||||
Based on the payload provided by the user.
|
Based on the payload provided by the user.
|
||||||
"""
|
"""
|
||||||
arananKelime: Optional[str] = Field("", description="""Keyword to search for with advanced operators support:
|
arananKelime: Optional[str] = Field("", description="Turkish keywords (supports +word -word \"phrase\" operators)")
|
||||||
• Simple words: 'arsa payı' (OR logic - finds documents with ANY word)
|
|
||||||
• Exact phrases: '"arsa payı"' (finds exact phrase)
|
|
||||||
• AND logic: 'arsa+payı' (both words required)
|
|
||||||
• Wildcards: 'bozma*' (matches bozma, bozması, bozmanın, etc.)
|
|
||||||
• Multiple required: '+"arsa payı" +"bozma sebebi"'
|
|
||||||
• Exclusion: '+"arsa payı" -"inşaat sözleşmesi"'
|
|
||||||
Examples: arsa payı | "arsa payı" | +"mülkiyet hakkı" +"bozma sebebi" | hukuk*""")
|
|
||||||
# Department/Board selection - Complete Court of Cassation chamber hierarchy
|
# Department/Board selection - Complete Court of Cassation chamber hierarchy
|
||||||
birimYrgKurulDaire: Optional[str] = Field("ALL", description="""
|
birimYrgKurulDaire: Optional[str] = Field("ALL", description="Chamber (ALL or specific chamber name)")
|
||||||
Court of Cassation (Yargıtay) chamber/board selection. Options include:
|
|
||||||
- 'ALL' for all chambers
|
|
||||||
- Civil: 'Civil General Assembly (Hukuk Genel Kurulu)', '1st Civil Chamber (1. Hukuk Dairesi)' through '23rd Civil Chamber (23. Hukuk Dairesi)', 'Civil Chambers Presidents Board (Hukuk Daireleri Başkanlar Kurulu)'
|
|
||||||
- Criminal: 'Criminal General Assembly (Ceza Genel Kurulu)', '1st Criminal Chamber (1. Ceza Dairesi)' through '23rd Criminal Chamber (23. Ceza Dairesi)', 'Criminal Chambers Presidents Board (Ceza Daireleri Başkanlar Kurulu)'
|
|
||||||
- General: 'Grand General Assembly (Büyük Genel Kurulu)'
|
|
||||||
Total: 52 possible values (including 'ALL' for all chambers)
|
|
||||||
""")
|
|
||||||
birimYrgHukukDaire: Optional[str] = Field("", description="Legacy field - use birimYrgKurulDaire instead for chamber selection")
|
|
||||||
birimYrgCezaDaire: Optional[str] = Field("", description="Legacy field - use birimYrgKurulDaire instead for chamber selection")
|
|
||||||
|
|
||||||
esasYil: Optional[str] = Field("", description="""Case year for 'Esas No' filtering.
|
esasYil: Optional[str] = Field("", description="Case year (YYYY)")
|
||||||
Format: YYYY (e.g., '2024')
|
esasIlkSiraNo: Optional[str] = Field("", description="Start case no")
|
||||||
Use with sequence numbers for precise case targeting""")
|
esasSonSiraNo: Optional[str] = Field("", description="End case no")
|
||||||
esasIlkSiraNo: Optional[str] = Field("", description="""Starting sequence number for 'Esas No' range filtering.
|
|
||||||
Format: numeric string (e.g., '1', '100')
|
|
||||||
Use with esasSonSiraNo for range: cases 100-200 in specified year""")
|
|
||||||
esasSonSiraNo: Optional[str] = Field("", description="""Ending sequence number for 'Esas No' range filtering.
|
|
||||||
Format: numeric string (e.g., '500', '1000')
|
|
||||||
Creates range from esasIlkSiraNo to this number""")
|
|
||||||
|
|
||||||
kararYil: Optional[str] = Field("", description="""Decision year for 'Karar No' filtering.
|
kararYil: Optional[str] = Field("", description="Decision year (YYYY)")
|
||||||
Format: YYYY (e.g., '2024')
|
kararIlkSiraNo: Optional[str] = Field("", description="Start decision no")
|
||||||
Filters decisions by the year they were issued""")
|
kararSonSiraNo: Optional[str] = Field("", description="End decision no")
|
||||||
kararIlkSiraNo: Optional[str] = Field("", description="""Starting sequence number for 'Karar No' range filtering.
|
|
||||||
Format: numeric string (e.g., '1', '50')
|
|
||||||
Use with kararSonSiraNo for decision number ranges""")
|
|
||||||
kararSonSiraNo: Optional[str] = Field("", description="""Ending sequence number for 'Karar No' range filtering.
|
|
||||||
Format: numeric string (e.g., '100', '500')
|
|
||||||
Creates range from kararIlkSiraNo to this number""")
|
|
||||||
|
|
||||||
baslangicTarihi: Optional[str] = Field("", description="""Start date for decision search.
|
baslangicTarihi: Optional[str] = Field("", description="Start date (DD.MM.YYYY)")
|
||||||
Format: DD.MM.YYYY (e.g., '01.01.2024')
|
bitisTarihi: Optional[str] = Field("", description="End date (DD.MM.YYYY)")
|
||||||
Use with bitisTarihi for date range filtering
|
|
||||||
Examples: '01.01.2024', '15.06.2023'""")
|
|
||||||
bitisTarihi: Optional[str] = Field("", description="""End date for decision search.
|
|
||||||
Format: DD.MM.YYYY (e.g., '31.12.2024')
|
|
||||||
Creates date range from baslangicTarihi to this date
|
|
||||||
Examples: '31.12.2024', '30.06.2023'""")
|
|
||||||
|
|
||||||
siralama: Optional[str] = Field("3", description="""Sorting criteria for search results:
|
|
||||||
• '1': Esas No (Case Number) - sorts by case registration order
|
|
||||||
• '2': Karar No (Decision Number) - sorts by decision issuance order
|
|
||||||
• '3': Karar Tarihi (Decision Date) - sorts by chronological order [DEFAULT]
|
|
||||||
Recommended: Use '3' for most recent decisions first""")
|
|
||||||
siralamaDirection: Optional[str] = Field("desc", description="""Sorting direction for results:
|
|
||||||
• 'desc': Descending order (newest/highest first) [DEFAULT]
|
|
||||||
• 'asc': Ascending order (oldest/lowest first)
|
|
||||||
Most common: 'desc' for latest decisions first""")
|
|
||||||
|
|
||||||
pageSize: int = Field(10, ge=1, le=100, description="""Number of results per page.
|
pageSize: int = Field(10, ge=1, le=10, description="Results per page (1-100)")
|
||||||
Range: 1-100 results per page
|
pageNumber: int = Field(1, ge=1, description="Page number (1-indexed)")
|
||||||
Recommended: 10-50 for balanced performance and coverage
|
|
||||||
Large values (50-100) for comprehensive analysis""")
|
|
||||||
pageNumber: int = Field(1, ge=1, description="""Page number to retrieve (1-indexed).
|
|
||||||
Start with 1 for first page
|
|
||||||
Use with pageSize to navigate through large result sets
|
|
||||||
Example: pageSize=50, pageNumber=3 gets results 101-150""")
|
|
||||||
|
|
||||||
class YargitayApiDecisionEntry(BaseModel):
|
class YargitayApiDecisionEntry(BaseModel):
|
||||||
"""Model for an individual decision entry from the Yargitay API search response."""
|
"""Model for an individual decision entry from the Yargitay API search response."""
|
||||||
id: str # Unique system ID of the decision
|
id: str # Unique system ID of the decision
|
||||||
daire: Optional[str] = Field(None, description="The chamber (Daire) that made the decision.")
|
daire: Optional[str] = Field(None, description="Chamber")
|
||||||
esasNo: Optional[str] = Field(None, alias="esasNo", description="Case registry number (Esas No).")
|
esasNo: Optional[str] = Field(None, alias="esasNo", description="Case no")
|
||||||
kararNo: Optional[str] = Field(None, alias="kararNo", description="Decision number (Karar No).")
|
kararNo: Optional[str] = Field(None, alias="kararNo", description="Decision no")
|
||||||
kararTarihi: Optional[str] = Field(None, alias="kararTarihi", description="Date of the decision (Karar Tarihi).")
|
kararTarihi: Optional[str] = Field(None, alias="kararTarihi", description="Date")
|
||||||
arananKelime: Optional[str] = Field(None, alias="arananKelime", description="Matched keyword (Aranan Kelime) in the search result item.")
|
|
||||||
# 'index' and 'siraNo' from API response are not critical for MCP tool, so omitted for brevity
|
# 'index' and 'siraNo' from API response are not critical for MCP tool, so omitted for brevity
|
||||||
|
|
||||||
# This field will be populated by the client after fetching the search list
|
# This field will be populated by the client after fetching the search list
|
||||||
document_url: Optional[HttpUrl] = Field(None, description="Direct URL (Belge URL) to the decision document.")
|
document_url: Optional[HttpUrl] = Field(None, description="Document URL")
|
||||||
|
|
||||||
model_config = ConfigDict(populate_by_name=True) # To allow populating by alias from API response
|
model_config = ConfigDict(populate_by_name=True) # To allow populating by alias from API response
|
||||||
|
|
||||||
|
|
||||||
class YargitayApiResponseInnerData(BaseModel):
|
class YargitayApiResponseInnerData(BaseModel):
|
||||||
"""Model for the inner 'data' object in the Yargitay API search response."""
|
"""Model for the inner 'data' object in the Yargitay API search response."""
|
||||||
data: List[YargitayApiDecisionEntry]
|
data: List[YargitayApiDecisionEntry] = Field(default_factory=list)
|
||||||
# draw: Optional[int] = None # Typically used by DataTables, not essential for MCP
|
# draw: Optional[int] = None # Typically used by DataTables, not essential for MCP
|
||||||
recordsTotal: int # Total number of records matching the query
|
recordsTotal: int = Field(default=0) # Total number of records matching the query
|
||||||
recordsFiltered: int # Total number of records after filtering (usually same as recordsTotal)
|
recordsFiltered: int = Field(default=0) # Total number of records after filtering (usually same as recordsTotal)
|
||||||
|
|
||||||
class YargitayApiSearchResponse(BaseModel):
|
class YargitayApiSearchResponse(BaseModel):
|
||||||
"""Model for the complete search response from the Yargitay API."""
|
"""Model for the complete search response from the Yargitay API."""
|
||||||
data: YargitayApiResponseInnerData
|
data: Optional[YargitayApiResponseInnerData] = Field(default_factory=lambda: YargitayApiResponseInnerData())
|
||||||
# metadata: Optional[Dict[str, Any]] = None # Optional metadata from API
|
# metadata: Optional[Dict[str, Any]] = None # Optional metadata from API
|
||||||
|
|
||||||
class YargitayDocumentMarkdown(BaseModel):
|
class YargitayDocumentMarkdown(BaseModel):
|
||||||
"""Model for a Yargitay decision document, containing only Markdown content."""
|
"""Model for a Yargitay decision document, containing only Markdown content."""
|
||||||
id: str = Field(..., description="The unique ID (Belge Kimliği) of the document.")
|
id: str = Field(..., description="Document ID")
|
||||||
markdown_content: Optional[str] = Field(None, description="The decision content (Karar İçeriği) converted to Markdown.")
|
markdown_content: Optional[str] = Field(None, description="Content")
|
||||||
source_url: HttpUrl = Field(..., description="The source URL (Kaynak URL) of the original document.")
|
source_url: HttpUrl = Field(..., description="Source URL")
|
||||||
|
|
||||||
|
class CleanYargitayDecisionEntry(BaseModel):
|
||||||
|
"""Clean decision entry without arananKelime field to reduce token usage."""
|
||||||
|
id: str
|
||||||
|
daire: Optional[str] = Field(None, description="Chamber")
|
||||||
|
esasNo: Optional[str] = Field(None, description="Case no")
|
||||||
|
kararNo: Optional[str] = Field(None, description="Decision no")
|
||||||
|
kararTarihi: Optional[str] = Field(None, description="Date")
|
||||||
|
document_url: Optional[HttpUrl] = Field(None, description="Document URL")
|
||||||
|
|
||||||
class CompactYargitaySearchResult(BaseModel):
|
class CompactYargitaySearchResult(BaseModel):
|
||||||
"""A more compact search result model for the MCP tool to return."""
|
"""A more compact search result model for the MCP tool to return."""
|
||||||
decisions: List[YargitayApiDecisionEntry]
|
decisions: List[CleanYargitayDecisionEntry]
|
||||||
total_records: int
|
total_records: int
|
||||||
requested_page: int
|
requested_page: int
|
||||||
page_size: int
|
page_size: int
|
||||||
Reference in New Issue
Block a user