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 |
+21
@@ -192,3 +192,24 @@ 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_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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -27,12 +29,14 @@ Bu proje, çeşitli Türk hukuk kaynaklarına (Yargıtay, Danıştay, Emsal Kara
|
|||||||
* **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ı)
|
* **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.
|
||||||
|
|
||||||
@@ -56,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**.
|
||||||
@@ -79,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:
|
||||||
|
|
||||||
@@ -121,42 +130,38 @@ 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 **30 MCP aracı** sunar:
|
<details>
|
||||||
|
<summary>🛠️ <strong>Kullanılabilir Araçlar (MCP Tools)</strong></summary>
|
||||||
|
|
||||||
### **Yargıtay Araçları (Ana API + 52 Daire Filtreleme)**
|
Bu FastMCP sunucusu **19 optimize edilmiş MCP aracı** sunar (token verimliliği için optimize edilmiş):
|
||||||
1. `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)
|
|
||||||
2. `get_yargitay_document_markdown(id: str)`: Belirli bir Yargıtay kararının metnini Markdown formatında getirir.
|
|
||||||
|
|
||||||
### **Danıştay Araçları (Dual API + 27 Daire Filtreleme)**
|
### **Yargıtay Araçları (Birleşik Bedesten API - Token Optimized)**
|
||||||
3. `search_danistay_by_keyword(andKelimeler, orKelimeler, ...)`: Danıştay kararlarını anahtar kelimelerle arar.
|
*Not: Yargıtay araçları token verimliliği için birleşik Bedesten API'ye entegre edilmiştir*
|
||||||
4. `search_danistay_detailed(daire, esasYil, ...)`: Danıştay kararlarını detaylı kriterlerle arar.
|
|
||||||
5. `get_danistay_document_markdown(id: str)`: Belirli bir Danıştay kararının metnini Markdown formatında getirir.
|
|
||||||
|
|
||||||
### **Birleşik Bedesten API Araçları (5 Mahkeme)**
|
### **Danıştay Araçları (Birleşik Bedesten API - Token Optimized)**
|
||||||
6. `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**
|
*Not: Danıştay araçları token verimliliği için birleşik Bedesten API'ye entegre edilmiştir*
|
||||||
7. `get_bedesten_document_markdown(documentId: str)`: Bedesten API'den herhangi bir belgeyi Markdown formatında getirir (HTML/PDF → Markdown)
|
|
||||||
|
### **Birleşik Bedesten API Araçları (5 Mahkeme) - 🚀 TOKEN OPTİMİZE**
|
||||||
|
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**
|
||||||
|
2. `get_bedesten_document_markdown(documentId: str)`: Bedesten API'den herhangi bir belgeyi Markdown formatında getirir (HTML/PDF → Markdown)
|
||||||
|
|
||||||
### **Emsal Karar Araçları (UYAP)**
|
### **Emsal Karar Araçları (UYAP)**
|
||||||
8. `search_emsal_detailed_decisions(keyword, ...)`: Emsal (UYAP) kararlarını detaylı kriterlerle arar.
|
3. `search_emsal_detailed_decisions(keyword, ...)`: Emsal (UYAP) kararlarını detaylı kriterlerle arar.
|
||||||
9. `get_emsal_document_markdown(id: str)`: Belirli bir Emsal kararının metnini Markdown formatında getirir.
|
4. `get_emsal_document_markdown(id: str)`: Belirli bir Emsal kararının metnini Markdown formatında getirir.
|
||||||
|
|
||||||
### **Uyuşmazlık Mahkemesi Araçları**
|
### **Uyuşmazlık Mahkemesi Araçları**
|
||||||
10. `search_uyusmazlik_decisions(icerik, ...)`: Uyuşmazlık Mahkemesi kararlarını çeşitli form kriterleriyle arar.
|
5. `search_uyusmazlik_decisions(icerik, ...)`: Uyuşmazlık Mahkemesi kararlarını çeşitli form kriterleriyle arar.
|
||||||
11. `get_uyusmazlik_document_markdown_from_url(document_url)`: Bir Uyuşmazlık kararını tam URL'sinden alıp Markdown formatında 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 Araçları (Norm Denetimi)**
|
### **Anayasa Mahkemesi Araçları (Birleşik API) - 🚀 TOKEN OPTİMİZE**
|
||||||
12. `search_anayasa_norm_denetimi_decisions(keywords_all, ...)`: AYM Norm Denetimi kararlarını kapsamlı kriterlerle arar.
|
7. `search_anayasa_unified(decision_type, keywords_all, ...)`: AYM kararlarını birleşik arama (Norm Denetimi + Bireysel Başvuru) - **4 araç → 2 araç optimizasyonu**
|
||||||
13. `get_anayasa_norm_denetimi_document_markdown(document_url, page_number)`: Belirli bir AYM Norm Denetimi kararını URL'sinden alır ve **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
|
||||||
|
|
||||||
### **Anayasa Mahkemesi Araçları (Bireysel Başvuru)**
|
|
||||||
14. `search_anayasa_bireysel_basvuru_report(keywords, ...)`: AYM Bireysel Başvuru "Karar Arama Raporu" oluşturur.
|
|
||||||
15. `get_anayasa_bireysel_basvuru_document_markdown(document_url_path, page_number)`: Belirli bir AYM Bireysel Başvuru kararını URL path'inden alır ve **sayfalanmış Markdown** içeriğini getirir.
|
|
||||||
|
|
||||||
### **KİK (Kamu İhale Kurulu) Araçları**
|
### **KİK (Kamu İhale Kurulu) Araçları**
|
||||||
16. `search_kik_decisions(karar_tipi, ...)`: KİK (Kamu İhale Kurulu) kararlarını arar.
|
9. `search_kik_decisions(karar_tipi, ...)`: KİK (Kamu İhale Kurulu) kararlarını arar.
|
||||||
17. `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.
|
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.
|
||||||
@@ -176,12 +181,26 @@ Bu FastMCP sunucusu **30 MCP aracı** sunar:
|
|||||||
* `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**
|
* `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)
|
* `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>
|
||||||
|
<summary>📊 <strong>Kapsamlı İstatistikler & Optimizasyon Başarıları</strong></summary>
|
||||||
|
|
||||||
|
🚀 **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 Mahkeme/Kurum:** 13 farklı hukuki kurum (KVKK dahil)
|
||||||
- **Toplam MCP Tool:** 30 arama ve belge getirme aracı
|
- **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:** Birleşik 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:** Birleşik 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ı)
|
||||||
@@ -210,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:
|
||||||
|
|
||||||
@@ -234,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, ge=1, le=10, description="Results per page.")
|
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,8 +4,8 @@ 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
|
||||||
|
|
||||||
# Court Type Options for Unified Search
|
# Court Type Options for Unified Search
|
||||||
BedestenCourtTypeEnum = Literal[
|
BedestenCourtTypeEnum = Literal[
|
||||||
@@ -16,37 +16,17 @@ BedestenCourtTypeEnum = Literal[
|
|||||||
"KYB" # Extraordinary Appeals (Kanun Yararına Bozma)
|
"KYB" # Extraordinary Appeals (Kanun Yararına Bozma)
|
||||||
]
|
]
|
||||||
|
|
||||||
# Danıştay Chamber/Board Options
|
|
||||||
DanistayBirimEnum = Literal[
|
|
||||||
"ALL", # "ALL" for all chambers
|
|
||||||
# Main Councils
|
|
||||||
"Büyük Gen.Kur.", # Grand General Assembly
|
|
||||||
"İdare Dava Daireleri Kurulu", # Administrative Cases Chambers Council
|
|
||||||
"Vergi Dava Daireleri Kurulu", # Tax Cases Chambers Council
|
|
||||||
"İç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="Results per page (1-10)")
|
pageSize: int = Field(..., description="Results per page (1-10)")
|
||||||
pageNumber: int = Field(..., description="Page number (1-indexed)")
|
pageNumber: int = Field(..., description="Page number (1-indexed)")
|
||||||
itemTypeList: List[str] = Field(..., description="Court type filter (YARGITAYKARARI/DANISTAYKARAR/YERELHUKUK/ISTINAFHUKUK/KYB)")
|
itemTypeList: List[str] = Field(..., description="Court type filter (YARGITAYKARARI/DANISTAYKARAR/YERELHUKUK/ISTINAFHUKUK/KYB)")
|
||||||
phrase: str = Field(..., description="Search phrase (use \"exact phrase\" for precise matching)")
|
phrase: str = Field(..., description="Search phrase. Supports: 'word', \"exact phrase\", +required, -exclude, AND/OR/NOT operators. No wildcards or regex.")
|
||||||
birimAdi: Optional[Union[YargitayBirimEnum, DanistayBirimEnum]] = Field(None, description="Chamber filter (optional)")
|
birimAdi: BirimAdiEnum = Field("ALL", description="""
|
||||||
|
Chamber filter (optional). Abbreviated values with Turkish names:
|
||||||
|
• 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)
|
||||||
|
• 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)
|
||||||
|
""")
|
||||||
kararTarihiStart: Optional[str] = Field(None, description="Start date (ISO 8601 format)")
|
kararTarihiStart: Optional[str] = Field(None, description="Start date (ISO 8601 format)")
|
||||||
kararTarihiEnd: Optional[str] = Field(None, description="End date (ISO 8601 format)")
|
kararTarihiEnd: Optional[str] = Field(None, description="End date (ISO 8601 format)")
|
||||||
sortFields: List[str] = Field(default=["KARAR_TARIHI"], description="Sort fields")
|
sortFields: List[str] = Field(default=["KARAR_TARIHI"], description="Sort fields")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
daire: str = Field("", description="Chamber")
|
||||||
esasYil: Optional[str] = Field(None, description="Case year")
|
esasYil: str = Field("", description="Case year")
|
||||||
esasIlkSiraNo: Optional[str] = Field(None, description="Start case no")
|
esasIlkSiraNo: str = Field("", description="Start case no")
|
||||||
esasSonSiraNo: Optional[str] = Field(None, description="End case no")
|
esasSonSiraNo: str = Field("", description="End case no")
|
||||||
kararYil: Optional[str] = Field(None, description="Decision year")
|
kararYil: str = Field("", description="Decision year")
|
||||||
kararIlkSiraNo: Optional[str] = Field(None, description="Start decision no")
|
kararIlkSiraNo: str = Field("", description="Start decision no")
|
||||||
kararSonSiraNo: Optional[str] = Field(None, description="End decision no")
|
kararSonSiraNo: str = Field("", description="End decision no")
|
||||||
baslangicTarihi: Optional[str] = Field(None, description="Start date")
|
baslangicTarihi: str = Field("", description="Start date")
|
||||||
bitisTarihi: Optional[str] = Field(None, description="End date")
|
bitisTarihi: str = Field("", description="End date")
|
||||||
mevzuatNumarasi: Optional[str] = Field(None, description="Law number")
|
mevzuatNumarasi: str = Field("", description="Law number")
|
||||||
mevzuatAdi: Optional[str] = Field(None, description="Law name")
|
mevzuatAdi: str = Field("", description="Law name")
|
||||||
madde: Optional[str] = Field(None, description="Article")
|
madde: str = Field("", description="Article")
|
||||||
siralama: str = Field("1", description="Sort by")
|
|
||||||
siralamaDirection: str = Field("desc", description="Direction")
|
|
||||||
# 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,11 +74,11 @@ 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="Chamber")
|
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="Keyword")
|
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
|
||||||
|
|
||||||
@@ -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")
|
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)
|
||||||
|
|||||||
+22
-22
@@ -12,8 +12,8 @@ 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.
|
||||||
|
|
||||||
@@ -36,22 +36,22 @@ 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")
|
keyword: str = Field("", description="Keyword")
|
||||||
|
|
||||||
selected_bam_civil_court: Optional[str] = Field(None, description="BAM Civil Court")
|
selected_bam_civil_court: str = Field("", description="BAM Civil Court")
|
||||||
selected_civil_court: Optional[str] = Field(None, description="Civil Court")
|
selected_civil_court: str = Field("", description="Civil Court")
|
||||||
selected_regional_civil_chambers: Optional[List[str]] = Field(default_factory=list, description="Regional chambers")
|
selected_regional_civil_chambers: List[str] = Field(default_factory=list, description="Regional chambers")
|
||||||
|
|
||||||
case_year_esas: Optional[str] = Field(None, description="Case year")
|
case_year_esas: str = Field("", description="Case year")
|
||||||
case_start_seq_esas: Optional[str] = Field(None, description="Start case no")
|
case_start_seq_esas: str = Field("", description="Start case no")
|
||||||
case_end_seq_esas: Optional[str] = Field(None, description="End case no")
|
case_end_seq_esas: str = Field("", description="End case no")
|
||||||
|
|
||||||
decision_year_karar: Optional[str] = Field(None, description="Decision year")
|
decision_year_karar: str = Field("", description="Decision year")
|
||||||
decision_start_seq_karar: Optional[str] = Field(None, description="Start decision no")
|
decision_start_seq_karar: str = Field("", description="Start decision no")
|
||||||
decision_end_seq_karar: Optional[str] = Field(None, description="End decision no")
|
decision_end_seq_karar: str = Field("", description="End decision no")
|
||||||
|
|
||||||
start_date: Optional[str] = Field(None, description="Start date (DD.MM.YYYY)")
|
start_date: str = Field("", description="Start date (DD.MM.YYYY)")
|
||||||
end_date: Optional[str] = Field(None, description="End date (DD.MM.YYYY)")
|
end_date: str = Field("", description="End date (DD.MM.YYYY)")
|
||||||
|
|
||||||
sort_criteria: str = Field("1", description="Sort by")
|
sort_criteria: str = Field("1", description="Sort by")
|
||||||
sort_direction: str = Field("desc", description="Direction")
|
sort_direction: str = Field("desc", description="Direction")
|
||||||
@@ -63,12 +63,12 @@ class EmsalSearchRequest(BaseModel): # This is the model the MCP tool will accep
|
|||||||
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="Chamber")
|
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="Keyword")
|
arananKelime: str = Field("", description="Keyword")
|
||||||
durum: Optional[str] = Field(None, description="Status")
|
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="Document URL")
|
document_url: Optional[HttpUrl] = Field(None, description="Document URL")
|
||||||
@@ -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")
|
||||||
|
|||||||
+13
-13
@@ -13,16 +13,16 @@ 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")
|
karar_tipi: KikKararTipi = Field(KikKararTipi.UYUSMAZLIK, description="Type")
|
||||||
karar_no: Optional[str] = Field(None, description="No")
|
karar_no: str = Field("", description="No")
|
||||||
karar_tarihi_baslangic: Optional[str] = Field(None, description="Start", 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="End", 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="Gazette")
|
resmi_gazete_sayisi: str = Field("", description="Gazette")
|
||||||
resmi_gazete_tarihi: Optional[str] = Field(None, description="Date", 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="Subject")
|
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="Entity")
|
ihaleyi_yapan_idare: str = Field("", description="Entity")
|
||||||
yil: Optional[str] = Field(None, description="Year")
|
yil: str = Field("", description="Year")
|
||||||
karar_metni: Optional[str] = Field(None, description="Text")
|
karar_metni: str = Field("", description="Text")
|
||||||
page: int = Field(1, ge=1, description="Page")
|
page: int = Field(1, ge=1, description="Page")
|
||||||
|
|
||||||
class KikDecisionEntry(BaseModel):
|
class KikDecisionEntry(BaseModel):
|
||||||
@@ -32,9 +32,9 @@ class KikDecisionEntry(BaseModel):
|
|||||||
karar_tipi: KikKararTipi = Field(..., description="Decision type")
|
karar_tipi: KikKararTipi = Field(..., description="Decision type")
|
||||||
|
|
||||||
karar_tarihi_str: str = Field(..., alias="kararTarihi", description="Date")
|
karar_tarihi_str: str = Field(..., alias="kararTarihi", description="Date")
|
||||||
idare_str: Optional[str] = Field(None, alias="idare", description="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="Subject")
|
ihale_konusu_str: str = Field("", alias="ihaleKonusu", description="Subject")
|
||||||
|
|
||||||
@computed_field
|
@computed_field
|
||||||
@property
|
@property
|
||||||
|
|||||||
+739
-405
File diff suppressed because it is too large
Load Diff
+3
-2
@@ -1,12 +1,12 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "yargi-mcp"
|
name = "yargi-mcp"
|
||||||
version = "0.1.4"
|
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",
|
||||||
@@ -30,6 +30,7 @@ dependencies = [
|
|||||||
"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,31 +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="Title")
|
sayfaAdi: str = Field("", description="Title")
|
||||||
YayinlanmaTarihi: Optional[str] = Field(None, description="Date")
|
YayinlanmaTarihi: str = Field("", description="Date")
|
||||||
PdfText: Optional[str] = Field(None, description="Text")
|
PdfText: str = Field("", description="Text")
|
||||||
KararTuruID: Optional[RekabetKararTuruGuidEnum] = Field(RekabetKararTuruGuidEnum.TUMU, description="Type")
|
KararTuruID: RekabetKararTuruGuidEnum = Field(RekabetKararTuruGuidEnum.TUMU, description="Type")
|
||||||
KararSayisi: Optional[str] = Field(None, description="No")
|
KararSayisi: str = Field("", description="No")
|
||||||
KararTarihi: Optional[str] = Field(None, description="Date")
|
KararTarihi: str = Field("", description="Date")
|
||||||
page: int = Field(1, ge=1, description="Page")
|
page: int = Field(1, ge=1, description="Page")
|
||||||
|
|
||||||
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="Pub date")
|
publication_date: str = Field("", description="Pub date")
|
||||||
decision_number: Optional[str] = Field(None, description="Number")
|
decision_number: str = Field("", description="Number")
|
||||||
decision_date: Optional[str] = Field(None, description="Date")
|
decision_date: str = Field("", description="Date")
|
||||||
decision_type_text: Optional[str] = Field(None, description="Type")
|
decision_type_text: str = Field("", description="Type")
|
||||||
title: Optional[str] = Field(None, description="Title")
|
title: str = Field("", description="Title")
|
||||||
decision_url: Optional[HttpUrl] = Field(None, description="URL")
|
decision_url: str = Field("", description="URL")
|
||||||
karar_id: Optional[str] = Field(None, description="ID")
|
karar_id: str = Field("", description="ID")
|
||||||
related_cases_url: Optional[HttpUrl] = Field(None, description="Cases URL")
|
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")
|
total_records_found: int = Field(0, description="Total")
|
||||||
retrieved_page_number: int = Field(description="Page")
|
retrieved_page_number: int = Field(description="Page")
|
||||||
total_pages: Optional[int] = Field(None, description="Pages")
|
total_pages: int = Field(0, description="Pages")
|
||||||
|
|
||||||
class RekabetDocument(BaseModel):
|
class RekabetDocument(BaseModel):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,14 +23,14 @@ 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 no")
|
karar_no: str = Field("", description="Decision no")
|
||||||
karar_ek: Optional[str] = Field(None, description="Appendix no")
|
karar_ek: str = Field("", description="Appendix no")
|
||||||
|
|
||||||
karar_tarih_baslangic: Optional[str] = Field(None, description="Start year (YYYY)")
|
karar_tarih_baslangic: str = Field("", description="Start year (YYYY)")
|
||||||
|
|
||||||
karar_tarih_bitis: Optional[str] = Field(None, description="End year")
|
karar_tarih_bitis: str = Field("", description="End year")
|
||||||
|
|
||||||
karar_tamami: Optional[str] = Field(None, description="Value")
|
karar_tamami: str = Field("", description="Value")
|
||||||
|
|
||||||
# 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)")
|
||||||
@@ -56,19 +63,19 @@ class TemyizKuruluSearchRequest(BaseModel):
|
|||||||
"""
|
"""
|
||||||
ilam_dairesi: DaireEnum = Field("ALL", description="Value")
|
ilam_dairesi: DaireEnum = Field("ALL", description="Value")
|
||||||
|
|
||||||
yili: Optional[str] = Field(None, description="Value")
|
yili: str = Field("", description="Value")
|
||||||
|
|
||||||
karar_tarih_baslangic: Optional[str] = Field(None, description="Value")
|
karar_tarih_baslangic: str = Field("", description="Value")
|
||||||
|
|
||||||
karar_tarih_bitis: Optional[str] = Field(None, description="End year")
|
karar_tarih_bitis: str = Field("", description="End year")
|
||||||
|
|
||||||
kamu_idaresi_turu: KamuIdaresiTuruEnum = Field("ALL", description="Value")
|
kamu_idaresi_turu: KamuIdaresiTuruEnum = Field("ALL", description="Value")
|
||||||
|
|
||||||
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="Value")
|
temyiz_karar: str = Field("", description="Value")
|
||||||
|
|
||||||
web_karar_konusu: WebKararKonusuEnum = Field("ALL", description="Value")
|
web_karar_konusu: WebKararKonusuEnum = Field("ALL", description="Value")
|
||||||
|
|
||||||
@@ -103,19 +110,19 @@ class DaireSearchRequest(BaseModel):
|
|||||||
"""
|
"""
|
||||||
yargilama_dairesi: DaireEnum = Field("ALL", description="Value")
|
yargilama_dairesi: DaireEnum = Field("ALL", description="Value")
|
||||||
|
|
||||||
karar_tarih_baslangic: Optional[str] = Field(None, description="Value")
|
karar_tarih_baslangic: str = Field("", description="Value")
|
||||||
|
|
||||||
karar_tarih_bitis: Optional[str] = Field(None, description="End year")
|
karar_tarih_bitis: str = Field("", description="End year")
|
||||||
|
|
||||||
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="Value")
|
kamu_idaresi_turu: KamuIdaresiTuruEnum = Field("ALL", description="Value")
|
||||||
|
|
||||||
hesap_yili: Optional[str] = Field(None, description="Value")
|
hesap_yili: str = Field("", description="Value")
|
||||||
|
|
||||||
web_karar_konusu: WebKararKonusuEnum = Field("ALL", description="Value")
|
web_karar_konusu: WebKararKonusuEnum = Field("ALL", description="Value")
|
||||||
|
|
||||||
web_karar_metni: Optional[str] = Field(None, description="Value")
|
web_karar_metni: str = Field("", description="Value")
|
||||||
|
|
||||||
# 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)")
|
||||||
@@ -127,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")
|
||||||
@@ -157,4 +164,57 @@ class SayistayDocumentMarkdown(BaseModel):
|
|||||||
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()
|
||||||
@@ -37,8 +37,6 @@ class YargitayDetailedSearchRequest(BaseModel):
|
|||||||
arananKelime: Optional[str] = Field("", description="Turkish keywords (supports +word -word \"phrase\" operators)")
|
arananKelime: Optional[str] = Field("", description="Turkish keywords (supports +word -word \"phrase\" operators)")
|
||||||
# Department/Board selection - Complete Court of Cassation chamber hierarchy
|
# Department/Board selection - Complete Court of Cassation chamber hierarchy
|
||||||
birimYrgKurulDaire: Optional[str] = Field("ALL", description="Chamber (ALL or specific chamber name)")
|
birimYrgKurulDaire: Optional[str] = Field("ALL", description="Chamber (ALL or specific chamber name)")
|
||||||
birimYrgHukukDaire: Optional[str] = Field("", description="Legacy field")
|
|
||||||
birimYrgCezaDaire: Optional[str] = Field("", description="Legacy field")
|
|
||||||
|
|
||||||
esasYil: Optional[str] = Field("", description="Case year (YYYY)")
|
esasYil: Optional[str] = Field("", description="Case year (YYYY)")
|
||||||
esasIlkSiraNo: Optional[str] = Field("", description="Start case no")
|
esasIlkSiraNo: Optional[str] = Field("", description="Start case no")
|
||||||
@@ -51,8 +49,6 @@ class YargitayDetailedSearchRequest(BaseModel):
|
|||||||
baslangicTarihi: Optional[str] = Field("", description="Start date (DD.MM.YYYY)")
|
baslangicTarihi: Optional[str] = Field("", description="Start date (DD.MM.YYYY)")
|
||||||
bitisTarihi: Optional[str] = Field("", description="End date (DD.MM.YYYY)")
|
bitisTarihi: Optional[str] = Field("", description="End date (DD.MM.YYYY)")
|
||||||
|
|
||||||
siralama: Optional[str] = Field("3", description="Sort by (1=case, 2=decision, 3=date)")
|
|
||||||
siralamaDirection: Optional[str] = Field("desc", description="Direction (asc/desc)")
|
|
||||||
|
|
||||||
pageSize: int = Field(10, ge=1, le=10, description="Results per page (1-100)")
|
pageSize: int = Field(10, ge=1, le=10, description="Results per page (1-100)")
|
||||||
pageNumber: int = Field(1, ge=1, description="Page number (1-indexed)")
|
pageNumber: int = Field(1, ge=1, description="Page number (1-indexed)")
|
||||||
|
|||||||
Reference in New Issue
Block a user