From 4c7da4fe10817bad0e8bc855c45c7a0e283f5fc4 Mon Sep 17 00:00:00 2001 From: saidsurucu Date: Sat, 28 Jun 2025 21:12:41 +0300 Subject: [PATCH] add sayistay module --- README.md | 20 +- mcp_server_main.py | 406 +++++++++++++++++++++- sayistay_mcp_module/__init__.py | 56 +++ sayistay_mcp_module/client.py | 594 ++++++++++++++++++++++++++++++++ sayistay_mcp_module/enums.py | 49 +++ sayistay_mcp_module/models.py | 242 +++++++++++++ 6 files changed, 1361 insertions(+), 6 deletions(-) create mode 100644 sayistay_mcp_module/__init__.py create mode 100644 sayistay_mcp_module/client.py create mode 100644 sayistay_mcp_module/enums.py create mode 100644 sayistay_mcp_module/models.py diff --git a/README.md b/README.md index 5693281..edc6cad 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Star History Chart](https://api.star-history.com/svg?repos=saidsurucu/yargi-mcp&type=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ı ve Rekabet Kurumu 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ı 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. ![örnek](./ornek.png) @@ -25,6 +25,7 @@ Bu proje, çeşitli Türk hukuk kaynaklarına (Yargıtay, Danıştay, Emsal Kara * **Anayasa Mahkemesi (Bireysel Başvuru):** Kapsamlı kriterlerle bireysel başvuru "Karar Arama Raporu" oluşturma ve listedeki kararların metinlerini (5.000 karakterlik) sayfalanmış Markdown formatında getirme. * **KİK (Kamu İhale Kurulu):** Çeşitli kriterlerle Kurul kararlarını arama; uzun karar metinlerini (varsayılan 5.000 karakterlik) sayfalanmış Markdown formatında getirme. * **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ı) * Karar metinlerinin daha kolay işlenebilmesi için Markdown formatına çevrilmesi. * Claude Desktop uygulaması ile `fastmcp install` komutu kullanılarak kolay entegrasyon. @@ -178,12 +179,23 @@ Bu FastMCP sunucusu aşağıdaki temel araçları sunar:     * `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. +--- + +* **Sayıştay Araçları (3 Karar Türü + 8 Daire Filtreleme):** + * `search_sayistay_genel_kurul(karar_no, karar_tarih_baslangic, karar_tamami, ...)`: Sayıştay Genel Kurul (yorumlayıcı) kararlarını arar. **Tarih aralığı** (2006-2024) + **İçerik arama** (400 karakter) + * `search_sayistay_temyiz_kurulu(ilam_dairesi, kamu_idaresi_turu, temyiz_karar, ...)`: Temyiz Kurulu (itiraz) kararlarını arar. **8 Daire filtreleme** + **Kurum türü** + **Konu sınıflandırması** + * `search_sayistay_daire(yargilama_dairesi, web_karar_metni, hesap_yili, ...)`: Daire (ilk derece denetim) kararlarını arar. **8 Daire filtreleme** + **Hesap yılı** + **İçerik arama** + * `get_sayistay_genel_kurul_document_markdown(decision_id: str)`: Genel Kurul kararının tam metnini Markdown formatında getirir + * `get_sayistay_temyiz_kurulu_document_markdown(decision_id: str)`: Temyiz Kurulu kararının tam metnini Markdown formatında getirir + * `get_sayistay_daire_document_markdown(decision_id: str)`: Daire kararının tam metnini Markdown formatında getirir + + --- ### **📊 Kapsamlı İstatistikler** -- **Toplam Mahkeme/Kurum:** 11 farklı hukuki kurum -- **Toplam MCP Tool:** 30+ arama ve belge getirme aracı -- **Daire/Kurul Filtreleme:** 79 farklı seçenek (52 Yargıtay + 27 Danıştay) +- **Toplam Mahkeme/Kurum:** 12 farklı hukuki kurum +- **Toplam MCP Tool:** 36+ arama ve belge getirme aracı +- **Daire/Kurul Filtreleme:** 87 farklı seçenek (52 Yargıtay + 27 Danıştay + 8 Sayıştay) - **Tarih Filtreleme:** 5 Bedesten API aracında ISO 8601 formatında tam tarih aralığı desteği - **Kesin Cümle Arama:** 5 Bedesten API aracında çift tırnak ile tam cümle arama (`"\"mülkiyet kararı\""` formatı) - **API Kaynağı:** Dual/Triple API desteği ile maksimum kapsama diff --git a/mcp_server_main.py b/mcp_server_main.py index b6acdf1..1931ce0 100644 --- a/mcp_server_main.py +++ b/mcp_server_main.py @@ -87,10 +87,19 @@ from rekabet_mcp_module.models import ( RekabetKararTuruGuidEnum ) +from sayistay_mcp_module.client import SayistayApiClient +from sayistay_mcp_module.models import ( + GenelKurulSearchRequest, GenelKurulSearchResponse, + TemyizKuruluSearchRequest, TemyizKuruluSearchResponse, + DaireSearchRequest, DaireSearchResponse, + SayistayDocumentMarkdown +) +from sayistay_mcp_module.enums import DaireEnum, KamuIdaresiTuruEnum, WebKararKonusuEnum + app = FastMCP( name="YargiMCP", - instructions="MCP server for TR legal databases (Yargitay, Danistay, Emsal, Uyusmazlik, Anayasa-Norm, Anayasa-Bireysel, KIK).", + instructions="MCP server for TR legal databases (Yargitay, Danistay, Emsal, Uyusmazlik, Anayasa-Norm, Anayasa-Bireysel, KIK, Sayistay).", dependencies=["httpx", "beautifulsoup4", "markitdown", "pydantic", "aiohttp", "playwright"] ) @@ -104,6 +113,7 @@ anayasa_bireysel_client_instance = AnayasaBireyselBasvuruApiClient() kik_client_instance = KikApiClient() rekabet_client_instance = RekabetKurumuApiClient() bedesten_client_instance = BedestenApiClient() +sayistay_client_instance = SayistayApiClient() KARAR_TURU_ADI_TO_GUID_ENUM_MAP = { @@ -1995,6 +2005,397 @@ async def get_kyb_bedesten_document_markdown( logger.exception("Error in tool 'get_kyb_bedesten_document_markdown'") raise +# --- MCP Tools for Sayıştay (Turkish Court of Accounts) --- + +@app.tool( + description="Search Sayıştay Genel Kurul (General Assembly) decisions - precedent-setting interpretive rulings by the Turkish Court of Accounts on audit and accountability regulations", + annotations={ + "readOnlyHint": True, + "openWorldHint": True, + "idempotentHint": True + } +) +async def search_sayistay_genel_kurul( + karar_no: Optional[str] = Field(None, description="Decision number to search for (e.g., '5415')"), + karar_ek: Optional[str] = Field(None, description="Decision appendix number (max 99, e.g., '1')"), + karar_tarih_baslangic: Optional[str] = Field(None, description=""" + Decision start year for date range filtering. + Available years: 2006-2024. Format: 'YYYY' (e.g., '2020') + Use with karar_tarih_bitis for date range filtering. + """), + karar_tarih_bitis: Optional[str] = Field(None, description=""" + Decision end year for date range filtering. + Available years: 2006-2024. Format: 'YYYY' (e.g., '2024') + Use with karar_tarih_baslangic for date range filtering. + """), + karar_tamami: Optional[str] = Field(None, description=""" + Content/text search within decision summaries (max 400 characters). + Searches in decision abstracts and main content. + Example: 'belediye taşınmaz tahsis' + """), + start: int = Field(0, description="Starting record for pagination (0-based)"), + length: int = Field(10, description="Number of records per page (1-100)") +) -> GenelKurulSearchResponse: + """ + Searches Sayıştay Genel Kurul (General Assembly) decisions. + + Genel Kurul decisions are the highest-level interpretive rulings from Turkey's + Court of Accounts, providing authoritative guidance on public audit standards, + accountability principles, and financial management regulations. + + Key Features: + • Decision number and appendix filtering + • Year-based date range filtering (2006-2024) + • Full-text content search in decision summaries + • Pagination support for large result sets + + Use Cases: + • Research audit precedents and interpretive guidance + • Find decisions on specific financial regulations + • Study evolution of public accountability standards + • Analyze Court of Accounts' institutional positions + """ + logger.info(f"Tool 'search_sayistay_genel_kurul' called with params: karar_no={karar_no}, karar_ek={karar_ek}, date_range={karar_tarih_baslangic}-{karar_tarih_bitis}, content={karar_tamami}") + + try: + search_request = GenelKurulSearchRequest( + karar_no=karar_no, + karar_ek=karar_ek, + karar_tarih_baslangic=karar_tarih_baslangic, + karar_tarih_bitis=karar_tarih_bitis, + karar_tamami=karar_tamami, + start=start, + length=length + ) + return await sayistay_client_instance.search_genel_kurul_decisions(search_request) + except Exception as e: + logger.exception("Error in tool 'search_sayistay_genel_kurul'") + raise + +@app.tool( + description="Search Sayıştay Temyiz Kurulu (Appeals Board) decisions - higher-level review of audit chamber findings and sanctions with chamber filtering (1-8), date filtering, and comprehensive search criteria", + annotations={ + "readOnlyHint": True, + "openWorldHint": True, + "idempotentHint": True + } +) +async def search_sayistay_temyiz_kurulu( + ilam_dairesi: DaireEnum = Field("ALL", description=""" + Chamber/Department filter for appeals board decisions. + • ALL: All chambers (default) + • 1-8: Specific chamber number (1. Daire through 8. Daire) + Each chamber specializes in different types of public institutions. + """), + yili: Optional[str] = Field(None, description=""" + Account year filter (Hesap Yılı). + Available years: 1993-2022. Format: 'YYYY' (e.g., '2020') + Refers to the fiscal year being audited, not decision date. + """), + karar_tarih_baslangic: Optional[str] = Field(None, description=""" + Decision start year for date range filtering. + Available years: 2000, 2006-2024. Format: 'YYYY' (e.g., '2020') + Use with karar_tarih_bitis for date range filtering. + """), + karar_tarih_bitis: Optional[str] = Field(None, description=""" + Decision end year for date range filtering. + Available years: 2000, 2006-2024. Format: 'YYYY' (e.g., '2024') + Use with karar_tarih_baslangic for date range filtering. + """), + kamu_idaresi_turu: KamuIdaresiTuruEnum = Field("ALL", description=""" + Public administration type filter: + • ALL: All institutions (default) + • Genel Bütçe Kapsamındaki İdareler: General budget administrations + • Yüksek Öğretim Kurumları: Higher education institutions + • Belediyeler ve Bağlı İdareler: Municipalities and affiliates + • Other specific institution types + """), + ilam_no: Optional[str] = Field(None, description="Audit report number (İlam No, max 50 chars)"), + dosya_no: Optional[str] = Field(None, description="File number for the case"), + temyiz_tutanak_no: Optional[str] = Field(None, description="Appeals board meeting minutes number"), + temyiz_karar: Optional[str] = Field(None, description=""" + Content search within appeals decisions. + Searches decision text and reasoning. + Example: 'araç kiralama kasko' + """), + web_karar_konusu: WebKararKonusuEnum = Field("ALL", description=""" + Decision subject category filter: + • ALL: All subjects (default) + • İhale Mevzuatı ile İlgili Kararlar: Procurement legislation + • Personel Mevzuatı ile İlgili Kararlar: Personnel legislation + • Harcırah Mevzuatı ile İlgili Kararlar: Travel allowance legislation + • Other specialized legal areas + """), + start: int = Field(0, description="Starting record for pagination (0-based)"), + length: int = Field(10, description="Number of records per page (1-100)") +) -> TemyizKuruluSearchResponse: + """ + Searches Sayıştay Temyiz Kurulu (Appeals Board) decisions. + + Temyiz Kurulu provides second-level review of audit chamber decisions, + examining appeals against sanctions and audit findings. These decisions + clarify audit standards and provide guidance on liability determinations. + + Key Features: + • Chamber-specific filtering (8 specialized audit chambers) + • Account year and decision date filtering (1993-2024) + • Public administration type categorization + • Subject matter classification and content search + • Case documentation tracking (ilam, dosya, tutanak numbers) + + Use Cases: + • Research appeals against specific audit findings + • Study chamber specialization patterns + • Analyze evolution of audit liability standards + • Find precedents for specific types of public institutions + """ + logger.info(f"Tool 'search_sayistay_temyiz_kurulu' called with params: chamber={ilam_dairesi}, year={yili}, admin_type={kamu_idaresi_turu}, subject={web_karar_konusu}") + + try: + search_request = TemyizKuruluSearchRequest( + ilam_dairesi=ilam_dairesi, + yili=yili, + karar_tarih_baslangic=karar_tarih_baslangic, + karar_tarih_bitis=karar_tarih_bitis, + kamu_idaresi_turu=kamu_idaresi_turu, + ilam_no=ilam_no, + dosya_no=dosya_no, + temyiz_tutanak_no=temyiz_tutanak_no, + temyiz_karar=temyiz_karar, + web_karar_konusu=web_karar_konusu, + start=start, + length=length + ) + return await sayistay_client_instance.search_temyiz_kurulu_decisions(search_request) + except Exception as e: + logger.exception("Error in tool 'search_sayistay_temyiz_kurulu'") + raise + +@app.tool( + description="Search Sayıştay Daire (Chamber) decisions - first-instance audit findings and sanctions from individual audit chambers with comprehensive filtering and subject categorization", + annotations={ + "readOnlyHint": True, + "openWorldHint": True, + "idempotentHint": True + } +) +async def search_sayistay_daire( + yargilama_dairesi: DaireEnum = Field("ALL", description=""" + Audit chamber filter: + • ALL: All chambers (default) + • 1-8: Specific chamber number (1. Daire through 8. Daire) + Each chamber audits different types of public institutions. + """), + karar_tarih_baslangic: Optional[str] = Field(None, description=""" + Decision start year for date range filtering. + Available years: 2012-2025. Format: 'YYYY' (e.g., '2020') + Use with karar_tarih_bitis for date range filtering. + """), + karar_tarih_bitis: Optional[str] = Field(None, description=""" + Decision end year for date range filtering. + Available years: 2012-2025. Format: 'YYYY' (e.g., '2024') + Use with karar_tarih_baslangic for date range filtering. + """), + ilam_no: Optional[str] = Field(None, description="Audit report number (İlam No, max 50 chars)"), + kamu_idaresi_turu: KamuIdaresiTuruEnum = Field("ALL", description=""" + Public administration type filter: + • ALL: All institutions (default) + • Genel Bütçe Kapsamındaki İdareler: General budget administrations + • Yüksek Öğretim Kurumları: Higher education institutions + • Belediyeler ve Bağlı İdareler: Municipalities and affiliates + • Other specific institution types + """), + hesap_yili: Optional[str] = Field(None, description=""" + Account year filter (Hesap Yılı). + Available years: 2005, 2008-2023. Format: 'YYYY' (e.g., '2020') + Refers to the fiscal year being audited, not decision date. + """), + web_karar_konusu: WebKararKonusuEnum = Field("ALL", description=""" + Decision subject category filter: + • ALL: All subjects (default) + • İhale Mevzuatı ile İlgili Kararlar: Procurement legislation + • Personel Mevzuatı ile İlgili Kararlar: Personnel legislation + • Vergi Resmi Harç ve Diğer Gelirlerle İlgili Kararlar: Tax and fee legislation + • Other specialized legal areas + """), + web_karar_metni: Optional[str] = Field(None, description=""" + Content search within chamber decisions. + Searches decision text and audit findings. + Example: 'birim fiyat revize edilmemesi' + """), + start: int = Field(0, description="Starting record for pagination (0-based)"), + length: int = Field(10, description="Number of records per page (1-100)") +) -> DaireSearchResponse: + """ + Searches Sayıştay Daire (Chamber) decisions. + + Chamber decisions represent first-instance audit findings, sanctions, and + liability determinations issued by specialized audit chambers. These form + the foundation of Turkey's public financial accountability system. + + Key Features: + • Chamber-specific filtering (8 specialized audit chambers) + • Decision and account year filtering (2012-2025) + • Public administration type categorization + • Subject matter classification and full-text search + • Audit report tracking and institutional analysis + + Use Cases: + • Research specific audit findings and sanctions + • Study chamber specialization and jurisdiction + • Analyze audit patterns by institution type + • Find precedents for financial irregularities + • Track audit evolution across fiscal years + """ + logger.info(f"Tool 'search_sayistay_daire' called with params: chamber={yargilama_dairesi}, admin_type={kamu_idaresi_turu}, subject={web_karar_konusu}, content={web_karar_metni}") + + try: + search_request = DaireSearchRequest( + yargilama_dairesi=yargilama_dairesi, + karar_tarih_baslangic=karar_tarih_baslangic, + karar_tarih_bitis=karar_tarih_bitis, + ilam_no=ilam_no, + kamu_idaresi_turu=kamu_idaresi_turu, + hesap_yili=hesap_yili, + web_karar_konusu=web_karar_konusu, + web_karar_metni=web_karar_metni, + start=start, + length=length + ) + return await sayistay_client_instance.search_daire_decisions(search_request) + except Exception as e: + logger.exception("Error in tool 'search_sayistay_daire'") + raise + +@app.tool( + description="Retrieve the full text of a Sayıştay Genel Kurul decision document in Markdown format for detailed analysis", + annotations={ + "readOnlyHint": True, + "openWorldHint": False, + "idempotentHint": True + } +) +async def get_sayistay_genel_kurul_document_markdown( + decision_id: str = Field(..., description="Decision ID from search_sayistay_genel_kurul results") +) -> SayistayDocumentMarkdown: + """ + Retrieves the full text of a Sayıştay Genel Kurul decision in Markdown format. + + This tool converts the original General Assembly decision document into clean, + readable Markdown format suitable for legal analysis and research. + + Input Requirements: + • decision_id: Use the ID from search_sayistay_genel_kurul results + • Decision ID must be non-empty string + + Output Format: + • Clean Markdown text with legal formatting preserved + • Structured content with reasoning and conclusions + • Removes technical artifacts from source documents + + Use for: + • Detailed analysis of audit precedents + • Research on public accountability standards + • Citation and reference building + • Legal interpretation and case study development + """ + logger.info(f"Tool 'get_sayistay_genel_kurul_document_markdown' called for ID: {decision_id}") + + if not decision_id or not decision_id.strip(): + raise ValueError("Decision ID must be a non-empty string.") + + try: + return await sayistay_client_instance.get_document_as_markdown(decision_id, "genel_kurul") + except Exception as e: + logger.exception("Error in tool 'get_sayistay_genel_kurul_document_markdown'") + raise + +@app.tool( + description="Retrieve the full text of a Sayıştay Temyiz Kurulu decision document in Markdown format for detailed appeals analysis", + annotations={ + "readOnlyHint": True, + "openWorldHint": False, + "idempotentHint": True + } +) +async def get_sayistay_temyiz_kurulu_document_markdown( + decision_id: str = Field(..., description="Decision ID from search_sayistay_temyiz_kurulu results") +) -> SayistayDocumentMarkdown: + """ + Retrieves the full text of a Sayıştay Temyiz Kurulu decision in Markdown format. + + This tool converts the original Appeals Board decision document into clean, + readable Markdown format for analysis of appellate reasoning and standards. + + Input Requirements: + • decision_id: Use the ID from search_sayistay_temyiz_kurulu results + • Decision ID must be non-empty string + + Output Format: + • Clean Markdown text with appellate reasoning preserved + • Structured content with original findings and appeals analysis + • Removes technical artifacts from source documents + + Use for: + • Analysis of appeals board reasoning and standards + • Research on audit liability determination evolution + • Understanding chamber decision review criteria + • Precedent analysis for audit appeal cases + """ + logger.info(f"Tool 'get_sayistay_temyiz_kurulu_document_markdown' called for ID: {decision_id}") + + if not decision_id or not decision_id.strip(): + raise ValueError("Decision ID must be a non-empty string.") + + try: + return await sayistay_client_instance.get_document_as_markdown(decision_id, "temyiz_kurulu") + except Exception as e: + logger.exception("Error in tool 'get_sayistay_temyiz_kurulu_document_markdown'") + raise + +@app.tool( + description="Retrieve the full text of a Sayıştay Daire decision document in Markdown format for detailed audit findings analysis", + annotations={ + "readOnlyHint": True, + "openWorldHint": False, + "idempotentHint": True + } +) +async def get_sayistay_daire_document_markdown( + decision_id: str = Field(..., description="Decision ID from search_sayistay_daire results") +) -> SayistayDocumentMarkdown: + """ + Retrieves the full text of a Sayıştay Daire decision in Markdown format. + + This tool converts the original chamber decision document into clean, + readable Markdown format for analysis of first-instance audit findings. + + Input Requirements: + • decision_id: Use the ID from search_sayistay_daire results + • Decision ID must be non-empty string + + Output Format: + • Clean Markdown text with audit findings preserved + • Structured content with violations and sanctions + • Removes technical artifacts from source documents + + Use for: + • Detailed analysis of audit findings and methodology + • Research on specific types of financial irregularities + • Understanding chamber jurisdiction and specialization + • Case study development for audit training and compliance + """ + logger.info(f"Tool 'get_sayistay_daire_document_markdown' called for ID: {decision_id}") + + if not decision_id or not decision_id.strip(): + raise ValueError("Decision ID must be a non-empty string.") + + try: + return await sayistay_client_instance.get_document_as_markdown(decision_id, "daire") + except Exception as e: + logger.exception("Error in tool 'get_sayistay_daire_document_markdown'") + raise + # --- Application Shutdown Handling --- def perform_cleanup(): logger.info("MCP Server performing cleanup...") @@ -2015,7 +2416,8 @@ def perform_cleanup(): globals().get('anayasa_bireysel_client_instance'), globals().get('kik_client_instance'), globals().get('rekabet_client_instance'), - globals().get('bedesten_client_instance') + globals().get('bedesten_client_instance'), + globals().get('sayistay_client_instance') ] async def close_all_clients_async(): tasks = [] diff --git a/sayistay_mcp_module/__init__.py b/sayistay_mcp_module/__init__.py new file mode 100644 index 0000000..e519775 --- /dev/null +++ b/sayistay_mcp_module/__init__.py @@ -0,0 +1,56 @@ +# sayistay_mcp_module/__init__.py + +""" +Sayıştay (Turkish Court of Accounts) MCP Module + +This module provides access to three types of Sayıştay decisions: +- Genel Kurul (General Assembly) decisions +- Temyiz Kurulu (Appeals Board) decisions +- Daire (Chamber) decisions + +The module handles ASP.NET WebForms authentication with CSRF tokens +and DataTables-based pagination for comprehensive decision search. +""" + +from .client import SayistayApiClient +from .models import ( + # Genel Kurul models + GenelKurulSearchRequest, + GenelKurulSearchResponse, + GenelKurulDecision, + + # Temyiz Kurulu models + TemyizKuruluSearchRequest, + TemyizKuruluSearchResponse, + TemyizKuruluDecision, + + # Daire models + DaireSearchRequest, + DaireSearchResponse, + DaireDecision, + + # Document models + SayistayDocumentMarkdown +) +from .enums import ( + DaireEnum, + KamuIdaresiTuruEnum, + WebKararKonusuEnum +) + +__all__ = [ + "SayistayApiClient", + "GenelKurulSearchRequest", + "GenelKurulSearchResponse", + "GenelKurulDecision", + "TemyizKuruluSearchRequest", + "TemyizKuruluSearchResponse", + "TemyizKuruluDecision", + "DaireSearchRequest", + "DaireSearchResponse", + "DaireDecision", + "SayistayDocumentMarkdown", + "DaireEnum", + "KamuIdaresiTuruEnum", + "WebKararKonusuEnum" +] \ No newline at end of file diff --git a/sayistay_mcp_module/client.py b/sayistay_mcp_module/client.py new file mode 100644 index 0000000..05d905c --- /dev/null +++ b/sayistay_mcp_module/client.py @@ -0,0 +1,594 @@ +# sayistay_mcp_module/client.py + +import httpx +import re +from bs4 import BeautifulSoup +from typing import Dict, Any, List, Optional, Tuple +import logging +import html +import tempfile +import os +from urllib.parse import urlencode, urljoin +from markitdown import MarkItDown + +from .models import ( + GenelKurulSearchRequest, GenelKurulSearchResponse, GenelKurulDecision, + TemyizKuruluSearchRequest, TemyizKuruluSearchResponse, TemyizKuruluDecision, + DaireSearchRequest, DaireSearchResponse, DaireDecision, + SayistayDocumentMarkdown +) +from .enums import DaireEnum, KamuIdaresiTuruEnum, WebKararKonusuEnum + +logger = logging.getLogger(__name__) +if not logger.hasHandlers(): + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + +class SayistayApiClient: + """ + API Client for Sayıştay (Turkish Court of Accounts) decision search system. + + Handles three types of decisions: + - Genel Kurul (General Assembly): Precedent-setting interpretive decisions + - Temyiz Kurulu (Appeals Board): Appeals against chamber decisions + - Daire (Chamber): First-instance audit findings and sanctions + + Features: + - ASP.NET WebForms session management with CSRF tokens + - DataTables-based pagination and filtering + - Automatic session refresh on expiration + - Document retrieval with Markdown conversion + """ + + BASE_URL = "https://www.sayistay.gov.tr" + + # Search endpoints for each decision type + GENEL_KURUL_ENDPOINT = "/KararlarGenelKurul/DataTablesList" + TEMYIZ_KURULU_ENDPOINT = "/KararlarTemyiz/DataTablesList" + DAIRE_ENDPOINT = "/KararlarDaire/DataTablesList" + + # Page endpoints for session initialization and document access + GENEL_KURUL_PAGE = "/KararlarGenelKurul" + TEMYIZ_KURULU_PAGE = "/KararlarTemyiz" + DAIRE_PAGE = "/KararlarDaire" + + def __init__(self, request_timeout: float = 60.0): + self.request_timeout = request_timeout + self.session_cookies: Dict[str, str] = {} + self.csrf_tokens: Dict[str, str] = {} # Store tokens for each endpoint + + self.http_client = httpx.AsyncClient( + base_url=self.BASE_URL, + headers={ + "Accept": "application/json, text/javascript, */*; q=0.01", + "Accept-Language": "tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7", + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", + "X-Requested-With": "XMLHttpRequest", + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "same-origin" + }, + timeout=request_timeout, + follow_redirects=True + ) + + async def _initialize_session_for_endpoint(self, endpoint_type: str) -> bool: + """ + Initialize session and obtain CSRF token for specific endpoint. + + Args: + endpoint_type: One of 'genel_kurul', 'temyiz_kurulu', 'daire' + + Returns: + True if session initialized successfully, False otherwise + """ + page_mapping = { + 'genel_kurul': self.GENEL_KURUL_PAGE, + 'temyiz_kurulu': self.TEMYIZ_KURULU_PAGE, + 'daire': self.DAIRE_PAGE + } + + if endpoint_type not in page_mapping: + logger.error(f"Invalid endpoint type: {endpoint_type}") + return False + + page_url = page_mapping[endpoint_type] + logger.info(f"Initializing session for {endpoint_type} endpoint: {page_url}") + + try: + response = await self.http_client.get(page_url) + response.raise_for_status() + + # Extract session cookies + for cookie_name, cookie_value in response.cookies.items(): + self.session_cookies[cookie_name] = cookie_value + logger.debug(f"Stored session cookie: {cookie_name}") + + # Extract CSRF token from form + soup = BeautifulSoup(response.text, 'html.parser') + csrf_input = soup.find('input', {'name': '__RequestVerificationToken'}) + + if csrf_input and csrf_input.get('value'): + self.csrf_tokens[endpoint_type] = csrf_input['value'] + logger.info(f"Extracted CSRF token for {endpoint_type}") + return True + else: + logger.warning(f"CSRF token not found in {endpoint_type} page") + return False + + except httpx.RequestError as e: + logger.error(f"HTTP error during session initialization for {endpoint_type}: {e}") + return False + except Exception as e: + logger.error(f"Error initializing session for {endpoint_type}: {e}") + return False + + def _enum_to_form_value(self, enum_value: str, enum_type: str) -> str: + """Convert enum values to form values expected by the API.""" + if enum_value == "ALL": + if enum_type == "daire": + return "Tüm Daireler" + elif enum_type == "kamu_idaresi": + return "Tüm Kurumlar" + elif enum_type == "web_karar_konusu": + return "Tüm Konular" + return enum_value + + def _build_datatables_params(self, start: int, length: int, draw: int = 1) -> List[Tuple[str, str]]: + """Build standard DataTables parameters for all endpoints.""" + params = [ + ("draw", str(draw)), + ("start", str(start)), + ("length", str(length)), + ("search[value]", ""), + ("search[regex]", "false") + ] + return params + + def _build_genel_kurul_form_data(self, params: GenelKurulSearchRequest, draw: int = 1) -> List[Tuple[str, str]]: + """Build form data for Genel Kurul search request.""" + form_data = self._build_datatables_params(params.start, params.length, draw) + + # Add DataTables column definitions (from actual request) + column_defs = [ + ("columns[0][data]", "KARARNO"), + ("columns[0][name]", ""), + ("columns[0][searchable]", "true"), + ("columns[0][orderable]", "false"), + ("columns[0][search][value]", ""), + ("columns[0][search][regex]", "false"), + + ("columns[1][data]", "KARARNO"), + ("columns[1][name]", ""), + ("columns[1][searchable]", "true"), + ("columns[1][orderable]", "true"), + ("columns[1][search][value]", ""), + ("columns[1][search][regex]", "false"), + + ("columns[2][data]", "KARARTARIH"), + ("columns[2][name]", ""), + ("columns[2][searchable]", "true"), + ("columns[2][orderable]", "true"), + ("columns[2][search][value]", ""), + ("columns[2][search][regex]", "false"), + + ("columns[3][data]", "KARAROZETI"), + ("columns[3][name]", ""), + ("columns[3][searchable]", "true"), + ("columns[3][orderable]", "false"), + ("columns[3][search][value]", ""), + ("columns[3][search][regex]", "false"), + + ("columns[4][data]", ""), + ("columns[4][name]", ""), + ("columns[4][searchable]", "true"), + ("columns[4][orderable]", "false"), + ("columns[4][search][value]", ""), + ("columns[4][search][regex]", "false"), + + ("order[0][column]", "2"), + ("order[0][dir]", "desc") + ] + form_data.extend(column_defs) + + # Add search parameters + form_data.extend([ + ("KararlarGenelKurulAra.KARARNO", params.karar_no or ""), + ("__Invariant[]", "KararlarGenelKurulAra.KARARNO"), + ("__Invariant[]", "KararlarGenelKurulAra.KARAREK"), + ("KararlarGenelKurulAra.KARAREK", params.karar_ek or ""), + ("KararlarGenelKurulAra.KARARTARIHBaslangic", params.karar_tarih_baslangic or "Başlangıç Tarihi"), + ("KararlarGenelKurulAra.KARARTARIHBitis", params.karar_tarih_bitis or "Bitiş Tarihi"), + ("KararlarGenelKurulAra.KARARTAMAMI", params.karar_tamami or ""), + ("__RequestVerificationToken", self.csrf_tokens.get('genel_kurul', '')) + ]) + + return form_data + + def _build_temyiz_kurulu_form_data(self, params: TemyizKuruluSearchRequest, draw: int = 1) -> List[Tuple[str, str]]: + """Build form data for Temyiz Kurulu search request.""" + form_data = self._build_datatables_params(params.start, params.length, draw) + + # Add DataTables column definitions (from actual request) + column_defs = [ + ("columns[0][data]", "TEMYIZTUTANAKTARIHI"), + ("columns[0][name]", ""), + ("columns[0][searchable]", "true"), + ("columns[0][orderable]", "false"), + ("columns[0][search][value]", ""), + ("columns[0][search][regex]", "false"), + + ("columns[1][data]", "TEMYIZTUTANAKTARIHI"), + ("columns[1][name]", ""), + ("columns[1][searchable]", "true"), + ("columns[1][orderable]", "true"), + ("columns[1][search][value]", ""), + ("columns[1][search][regex]", "false"), + + ("columns[2][data]", "ILAMDAIRESI"), + ("columns[2][name]", ""), + ("columns[2][searchable]", "true"), + ("columns[2][orderable]", "true"), + ("columns[2][search][value]", ""), + ("columns[2][search][regex]", "false"), + + ("columns[3][data]", "TEMYIZKARAR"), + ("columns[3][name]", ""), + ("columns[3][searchable]", "true"), + ("columns[3][orderable]", "false"), + ("columns[3][search][value]", ""), + ("columns[3][search][regex]", "false"), + + ("columns[4][data]", ""), + ("columns[4][name]", ""), + ("columns[4][searchable]", "true"), + ("columns[4][orderable]", "false"), + ("columns[4][search][value]", ""), + ("columns[4][search][regex]", "false"), + + ("order[0][column]", "1"), + ("order[0][dir]", "desc") + ] + form_data.extend(column_defs) + + # Add search parameters + daire_value = self._enum_to_form_value(params.ilam_dairesi, "daire") + kamu_idaresi_value = self._enum_to_form_value(params.kamu_idaresi_turu, "kamu_idaresi") + web_karar_konusu_value = self._enum_to_form_value(params.web_karar_konusu, "web_karar_konusu") + + form_data.extend([ + ("KararlarTemyizAra.ILAMDAIRESI", daire_value), + ("KararlarTemyizAra.YILI", params.yili or ""), + ("KararlarTemyizAra.KARARTRHBaslangic", params.karar_tarih_baslangic or ""), + ("KararlarTemyizAra.KARARTRHBitis", params.karar_tarih_bitis or ""), + ("KararlarTemyizAra.KAMUIDARESITURU", kamu_idaresi_value if kamu_idaresi_value != "Tüm Kurumlar" else ""), + ("KararlarTemyizAra.ILAMNO", params.ilam_no or ""), + ("KararlarTemyizAra.DOSYANO", params.dosya_no or ""), + ("KararlarTemyizAra.TEMYIZTUTANAKNO", params.temyiz_tutanak_no or ""), + ("__Invariant", "KararlarTemyizAra.TEMYIZTUTANAKNO"), + ("KararlarTemyizAra.TEMYIZKARAR", params.temyiz_karar or ""), + ("KararlarTemyizAra.WEBKARARKONUSU", web_karar_konusu_value if web_karar_konusu_value != "Tüm Konular" else ""), + ("__RequestVerificationToken", self.csrf_tokens.get('temyiz_kurulu', '')) + ]) + + return form_data + + def _build_daire_form_data(self, params: DaireSearchRequest, draw: int = 1) -> List[Tuple[str, str]]: + """Build form data for Daire search request.""" + form_data = self._build_datatables_params(params.start, params.length, draw) + + # Add DataTables column definitions (from actual request) + column_defs = [ + ("columns[0][data]", "YARGILAMADAIRESI"), + ("columns[0][name]", ""), + ("columns[0][searchable]", "true"), + ("columns[0][orderable]", "false"), + ("columns[0][search][value]", ""), + ("columns[0][search][regex]", "false"), + + ("columns[1][data]", "KARARTRH"), + ("columns[1][name]", ""), + ("columns[1][searchable]", "true"), + ("columns[1][orderable]", "true"), + ("columns[1][search][value]", ""), + ("columns[1][search][regex]", "false"), + + ("columns[2][data]", "KARARNO"), + ("columns[2][name]", ""), + ("columns[2][searchable]", "true"), + ("columns[2][orderable]", "true"), + ("columns[2][search][value]", ""), + ("columns[2][search][regex]", "false"), + + ("columns[3][data]", "YARGILAMADAIRESI"), + ("columns[3][name]", ""), + ("columns[3][searchable]", "true"), + ("columns[3][orderable]", "true"), + ("columns[3][search][value]", ""), + ("columns[3][search][regex]", "false"), + + ("columns[4][data]", "WEBKARARMETNI"), + ("columns[4][name]", ""), + ("columns[4][searchable]", "true"), + ("columns[4][orderable]", "false"), + ("columns[4][search][value]", ""), + ("columns[4][search][regex]", "false"), + + ("columns[5][data]", ""), + ("columns[5][name]", ""), + ("columns[5][searchable]", "true"), + ("columns[5][orderable]", "false"), + ("columns[5][search][value]", ""), + ("columns[5][search][regex]", "false"), + + ("order[0][column]", "2"), + ("order[0][dir]", "desc") + ] + form_data.extend(column_defs) + + # Add search parameters + daire_value = self._enum_to_form_value(params.yargilama_dairesi, "daire") + kamu_idaresi_value = self._enum_to_form_value(params.kamu_idaresi_turu, "kamu_idaresi") + web_karar_konusu_value = self._enum_to_form_value(params.web_karar_konusu, "web_karar_konusu") + + form_data.extend([ + ("KararlarDaireAra.YARGILAMADAIRESI", daire_value), + ("KararlarDaireAra.KARARTRHBaslangic", params.karar_tarih_baslangic or ""), + ("KararlarDaireAra.KARARTRHBitis", params.karar_tarih_bitis or ""), + ("KararlarDaireAra.ILAMNO", params.ilam_no or ""), + ("KararlarDaireAra.KAMUIDARESITURU", kamu_idaresi_value if kamu_idaresi_value != "Tüm Kurumlar" else ""), + ("KararlarDaireAra.HESAPYILI", params.hesap_yili or ""), + ("KararlarDaireAra.WEBKARARKONUSU", web_karar_konusu_value if web_karar_konusu_value != "Tüm Konular" else ""), + ("KararlarDaireAra.WEBKARARMETNI", params.web_karar_metni or ""), + ("__RequestVerificationToken", self.csrf_tokens.get('daire', '')) + ]) + + return form_data + + async def search_genel_kurul_decisions(self, params: GenelKurulSearchRequest) -> GenelKurulSearchResponse: + """ + Search Sayıştay Genel Kurul (General Assembly) decisions. + + Args: + params: Search parameters for Genel Kurul decisions + + Returns: + GenelKurulSearchResponse with matching decisions + """ + # Initialize session if needed + if 'genel_kurul' not in self.csrf_tokens: + if not await self._initialize_session_for_endpoint('genel_kurul'): + raise Exception("Failed to initialize session for Genel Kurul endpoint") + + form_data = self._build_genel_kurul_form_data(params) + encoded_data = urlencode(form_data, encoding='utf-8') + + logger.info(f"Searching Genel Kurul decisions with parameters: {params.model_dump(exclude_none=True)}") + + try: + # Update headers with cookies + headers = self.http_client.headers.copy() + if self.session_cookies: + cookie_header = "; ".join([f"{k}={v}" for k, v in self.session_cookies.items()]) + headers["Cookie"] = cookie_header + + response = await self.http_client.post( + self.GENEL_KURUL_ENDPOINT, + data=encoded_data, + headers=headers + ) + response.raise_for_status() + response_json = response.json() + + # Parse response + decisions = [] + for item in response_json.get('data', []): + decisions.append(GenelKurulDecision( + id=item['Id'], + karar_no=item['KARARNO'], + karar_tarih=item['KARARTARIH'], + karar_ozeti=item['KARAROZETI'] + )) + + return GenelKurulSearchResponse( + decisions=decisions, + total_records=response_json.get('recordsTotal', 0), + total_filtered=response_json.get('recordsFiltered', 0), + draw=response_json.get('draw', 1) + ) + + except httpx.RequestError as e: + logger.error(f"HTTP error during Genel Kurul search: {e}") + raise + except Exception as e: + logger.error(f"Error processing Genel Kurul search: {e}") + raise + + async def search_temyiz_kurulu_decisions(self, params: TemyizKuruluSearchRequest) -> TemyizKuruluSearchResponse: + """ + Search Sayıştay Temyiz Kurulu (Appeals Board) decisions. + + Args: + params: Search parameters for Temyiz Kurulu decisions + + Returns: + TemyizKuruluSearchResponse with matching decisions + """ + # Initialize session if needed + if 'temyiz_kurulu' not in self.csrf_tokens: + if not await self._initialize_session_for_endpoint('temyiz_kurulu'): + raise Exception("Failed to initialize session for Temyiz Kurulu endpoint") + + form_data = self._build_temyiz_kurulu_form_data(params) + encoded_data = urlencode(form_data, encoding='utf-8') + + logger.info(f"Searching Temyiz Kurulu decisions with parameters: {params.model_dump(exclude_none=True)}") + + try: + # Update headers with cookies + headers = self.http_client.headers.copy() + if self.session_cookies: + cookie_header = "; ".join([f"{k}={v}" for k, v in self.session_cookies.items()]) + headers["Cookie"] = cookie_header + + response = await self.http_client.post( + self.TEMYIZ_KURULU_ENDPOINT, + data=encoded_data, + headers=headers + ) + response.raise_for_status() + response_json = response.json() + + # Parse response + decisions = [] + for item in response_json.get('data', []): + decisions.append(TemyizKuruluDecision( + id=item['Id'], + temyiz_tutanak_tarihi=item['TEMYIZTUTANAKTARIHI'], + ilam_dairesi=item['ILAMDAIRESI'], + temyiz_karar=item['TEMYIZKARAR'] + )) + + return TemyizKuruluSearchResponse( + decisions=decisions, + total_records=response_json.get('recordsTotal', 0), + total_filtered=response_json.get('recordsFiltered', 0), + draw=response_json.get('draw', 1) + ) + + except httpx.RequestError as e: + logger.error(f"HTTP error during Temyiz Kurulu search: {e}") + raise + except Exception as e: + logger.error(f"Error processing Temyiz Kurulu search: {e}") + raise + + async def search_daire_decisions(self, params: DaireSearchRequest) -> DaireSearchResponse: + """ + Search Sayıştay Daire (Chamber) decisions. + + Args: + params: Search parameters for Daire decisions + + Returns: + DaireSearchResponse with matching decisions + """ + # Initialize session if needed + if 'daire' not in self.csrf_tokens: + if not await self._initialize_session_for_endpoint('daire'): + raise Exception("Failed to initialize session for Daire endpoint") + + form_data = self._build_daire_form_data(params) + encoded_data = urlencode(form_data, encoding='utf-8') + + logger.info(f"Searching Daire decisions with parameters: {params.model_dump(exclude_none=True)}") + + try: + # Update headers with cookies + headers = self.http_client.headers.copy() + if self.session_cookies: + cookie_header = "; ".join([f"{k}={v}" for k, v in self.session_cookies.items()]) + headers["Cookie"] = cookie_header + + response = await self.http_client.post( + self.DAIRE_ENDPOINT, + data=encoded_data, + headers=headers + ) + response.raise_for_status() + response_json = response.json() + + # Parse response + decisions = [] + for item in response_json.get('data', []): + decisions.append(DaireDecision( + id=item['Id'], + yargilama_dairesi=item['YARGILAMADAIRESI'], + karar_tarih=item['KARARTRH'], + karar_no=item['KARARNO'], + ilam_no=item.get('ILAMNO'), # Use get() to handle None values + madde_no=item['MADDENO'], + kamu_idaresi_turu=item['KAMUIDARESITURU'], + hesap_yili=item['HESAPYILI'], + web_karar_konusu=item['WEBKARARKONUSU'], + web_karar_metni=item['WEBKARARMETNI'] + )) + + return DaireSearchResponse( + decisions=decisions, + total_records=response_json.get('recordsTotal', 0), + total_filtered=response_json.get('recordsFiltered', 0), + draw=response_json.get('draw', 1) + ) + + except httpx.RequestError as e: + logger.error(f"HTTP error during Daire search: {e}") + raise + except Exception as e: + logger.error(f"Error processing Daire search: {e}") + raise + + def _convert_html_to_markdown(self, html_content: str) -> Optional[str]: + """Convert HTML content to Markdown using MarkItDown.""" + if not html_content: + return None + + temp_file_path = None + try: + md_converter = MarkItDown() + + # Write HTML to temp file + with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".html", encoding="utf-8") as tmp: + tmp.write(html_content) + temp_file_path = tmp.name + + # Convert + result = md_converter.convert(temp_file_path) + markdown_content = result.text_content + + logger.info("Successfully converted HTML to Markdown") + return markdown_content + + except Exception as e: + logger.error(f"Error converting HTML to Markdown: {e}") + return f"Error converting HTML content: {str(e)}" + finally: + if temp_file_path and os.path.exists(temp_file_path): + os.remove(temp_file_path) + + async def get_document_as_markdown(self, decision_id: str, decision_type: str) -> SayistayDocumentMarkdown: + """ + Retrieve full text of a Sayıştay decision and convert to Markdown. + + Note: This is a placeholder implementation. The actual document endpoints + would need to be discovered through further analysis of the website. + + Args: + decision_id: Unique decision identifier + decision_type: Type of decision ('genel_kurul', 'temyiz_kurulu', 'daire') + + Returns: + SayistayDocumentMarkdown with converted content + """ + logger.info(f"Retrieving document for {decision_type} decision ID: {decision_id}") + + # Document retrieval endpoints would need to be implemented based on + # actual website behavior - this would require analyzing the JavaScript + # or finding direct document links + + return SayistayDocumentMarkdown( + decision_id=decision_id, + decision_type=decision_type, + source_url=f"{self.BASE_URL}/document/{decision_id}", + markdown_content=None, + error_message="Document retrieval endpoint not yet implemented. Further analysis of the website's document access mechanism is required." + ) + + async def close_client_session(self): + """Close HTTP client session.""" + if hasattr(self, 'http_client') and self.http_client and not self.http_client.is_closed: + await self.http_client.aclose() + logger.info("SayistayApiClient: HTTP client session closed.") \ No newline at end of file diff --git a/sayistay_mcp_module/enums.py b/sayistay_mcp_module/enums.py new file mode 100644 index 0000000..5a62616 --- /dev/null +++ b/sayistay_mcp_module/enums.py @@ -0,0 +1,49 @@ +# sayistay_mcp_module/enums.py + +from typing import Literal + +# Chamber/Daire options for Temyiz Kurulu and Daire endpoints (1-8 + All) +DaireEnum = Literal[ + "ALL", # All chambers/departments + "1", # 1. Daire + "2", # 2. Daire + "3", # 3. Daire + "4", # 4. Daire + "5", # 5. Daire + "6", # 6. Daire + "7", # 7. Daire + "8" # 8. Daire +] + +# Public Administration Types (Kamu İdaresi Türü) +KamuIdaresiTuruEnum = Literal[ + "ALL", # All institutions + "Genel Bütçe Kapsamındaki İdareler", # General Budget Administrations + "Yüksek Öğretim Kurumları", # Higher Education Institutions + "Diğer Özel Bütçeli İdareler", # Other Special Budget Administrations + "Düzenleyici ve Denetleyici Kurumlar", # Regulatory and Supervisory Institutions + "Sosyal Güvenlik Kurumları", # Social Security Institutions + "Özel İdareler", # Special Administrations + "Belediyeler ve Bağlı İdareler", # Municipalities and Affiliated Administrations + "Diğer" # Other +] + +# Decision Subject Categories (Web Karar Konusu) +WebKararKonusuEnum = Literal[ + "ALL", # All subjects + "Harcırah Mevzuatı ile İlgili Kararlar", # Travel Allowance Legislation Related Decisions + "İhale Mevzuatı ile İlgili Kararlar", # Procurement Legislation Related Decisions + "İş Mevzuatı ile İlgili Kararlar", # Labor Legislation Related Decisions + "Personel Mevzuatı ile İlgili Kararlar", # Personnel Legislation Related Decisions + "Sorumluluk ve Yargılama Usulleri ile İlgili Kararlar", # Liability and Trial Procedures Related Decisions + "Vergi Resmi Harç ve Diğer Gelirlerle İlgili Kararlar", # Tax, Official Fee and Other Revenue Related Decisions + "Çeşitli Konuları İlgilendiren Kararlar" # Decisions Concerning Various Topics +] + +# Year ranges for different endpoints +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 +DAIRE_YEARS = [str(year) for year in range(2012, 2026)] # 2012-2025 + +# Account years for Temyiz Kurulu and Daire endpoints +HESAP_YILLARI = [str(year) for year in range(1993, 2024)] # 1993-2023 \ No newline at end of file diff --git a/sayistay_mcp_module/models.py b/sayistay_mcp_module/models.py new file mode 100644 index 0000000..d789383 --- /dev/null +++ b/sayistay_mcp_module/models.py @@ -0,0 +1,242 @@ +# sayistay_mcp_module/models.py + +from pydantic import BaseModel, Field +from typing import Optional, List, Union +from .enums import DaireEnum, KamuIdaresiTuruEnum, WebKararKonusuEnum + +# ============================================================================ +# Genel Kurul (General Assembly) Models +# ============================================================================ + +class GenelKurulSearchRequest(BaseModel): + """ + Search request for Sayıştay Genel Kurul (General Assembly) decisions. + + Genel Kurul decisions are precedent-setting rulings made by the full assembly + of the Turkish Court of Accounts, typically addressing interpretation of + audit and accountability regulations. + """ + karar_no: Optional[str] = Field(None, description="Decision number (e.g., '5415')") + karar_ek: Optional[str] = Field(None, description="Decision appendix number (max 99)") + + karar_tarih_baslangic: Optional[str] = Field(None, description=""" + Decision start year for date range filtering. + Available years: 2006-2024. Format: 'YYYY' (e.g., '2020') + Use with karar_tarih_bitis for date range filtering. + """) + + karar_tarih_bitis: Optional[str] = Field(None, description=""" + Decision end year for date range filtering. + Available years: 2006-2024. Format: 'YYYY' (e.g., '2024') + Use with karar_tarih_baslangic for date range filtering. + """) + + karar_tamami: Optional[str] = Field(None, description=""" + Content/text search within decision summaries (max 400 characters). + Searches in decision abstracts and main content. + Example: 'belediye taşınmaz tahsis' + """) + + # DataTables pagination + start: int = Field(0, description="Starting record for pagination (0-based)") + length: int = Field(10, description="Number of records per page (1-100)") + +class GenelKurulDecision(BaseModel): + """Single Genel Kurul decision entry from search results.""" + id: int = Field(..., description="Unique decision ID") + karar_no: str = Field(..., description="Decision number (e.g., '5415/1')") + karar_tarih: str = Field(..., description="Decision date in DD.MM.YYYY format") + karar_ozeti: str = Field(..., description="Decision summary/abstract") + +class GenelKurulSearchResponse(BaseModel): + """Response from Genel Kurul search endpoint.""" + decisions: List[GenelKurulDecision] = Field(default_factory=list, description="List of matching decisions") + total_records: int = Field(0, description="Total number of matching records") + total_filtered: int = Field(0, description="Number of records after filtering") + draw: int = Field(1, description="DataTables draw counter") + +# ============================================================================ +# Temyiz Kurulu (Appeals Board) Models +# ============================================================================ + +class TemyizKuruluSearchRequest(BaseModel): + """ + Search request for Sayıştay Temyiz Kurulu (Appeals Board) decisions. + + Temyiz Kurulu reviews appeals against audit chamber decisions, + providing higher-level review of audit findings and sanctions. + """ + ilam_dairesi: DaireEnum = Field("ALL", description=""" + Chamber/Department filter for appeals board decisions. + • ALL: All chambers (default) + • 1-8: Specific chamber number (1. Daire through 8. Daire) + Each chamber specializes in different types of public institutions. + """) + + yili: Optional[str] = Field(None, description=""" + Account year filter (Hesap Yılı). + Available years: 1993-2022. Format: 'YYYY' (e.g., '2020') + Refers to the fiscal year being audited, not decision date. + """) + + karar_tarih_baslangic: Optional[str] = Field(None, description=""" + Decision start year for date range filtering. + Available years: 2000, 2006-2024. Format: 'YYYY' (e.g., '2020') + Use with karar_tarih_bitis for date range filtering. + """) + + karar_tarih_bitis: Optional[str] = Field(None, description=""" + Decision end year for date range filtering. + Available years: 2000, 2006-2024. Format: 'YYYY' (e.g., '2024') + Use with karar_tarih_baslangic for date range filtering. + """) + + kamu_idaresi_turu: KamuIdaresiTuruEnum = Field("ALL", description=""" + Public administration type filter: + • ALL: All institutions (default) + • Genel Bütçe Kapsamındaki İdareler: General budget administrations + • Yüksek Öğretim Kurumları: Higher education institutions + • Belediyeler ve Bağlı İdareler: Municipalities and affiliates + • Other specific institution types + """) + + ilam_no: Optional[str] = Field(None, description="Audit report number (İlam No, max 50 chars)") + dosya_no: Optional[str] = Field(None, description="File number for the case") + temyiz_tutanak_no: Optional[str] = Field(None, description="Appeals board meeting minutes number") + + temyiz_karar: Optional[str] = Field(None, description=""" + Content search within appeals decisions. + Searches decision text and reasoning. + Example: 'araç kiralama kasko' + """) + + web_karar_konusu: WebKararKonusuEnum = Field("ALL", description=""" + Decision subject category filter: + • ALL: All subjects (default) + • İhale Mevzuatı ile İlgili Kararlar: Procurement legislation + • Personel Mevzuatı ile İlgili Kararlar: Personnel legislation + • Harcırah Mevzuatı ile İlgili Kararlar: Travel allowance legislation + • Other specialized legal areas + """) + + # DataTables pagination + start: int = Field(0, description="Starting record for pagination (0-based)") + length: int = Field(10, description="Number of records per page (1-100)") + +class TemyizKuruluDecision(BaseModel): + """Single Temyiz Kurulu decision entry from search results.""" + id: int = Field(..., description="Unique decision ID") + temyiz_tutanak_tarihi: str = Field(..., description="Appeals board meeting date in DD.MM.YYYY format") + ilam_dairesi: int = Field(..., description="Chamber number (1-8)") + temyiz_karar: str = Field(..., description="Appeals decision summary and reasoning") + +class TemyizKuruluSearchResponse(BaseModel): + """Response from Temyiz Kurulu search endpoint.""" + decisions: List[TemyizKuruluDecision] = Field(default_factory=list, description="List of matching appeals decisions") + total_records: int = Field(0, description="Total number of matching records") + total_filtered: int = Field(0, description="Number of records after filtering") + draw: int = Field(1, description="DataTables draw counter") + +# ============================================================================ +# Daire (Chamber) Models +# ============================================================================ + +class DaireSearchRequest(BaseModel): + """ + Search request for Sayıştay Daire (Chamber) decisions. + + Daire decisions are first-instance audit findings and sanctions + issued by individual audit chambers before potential appeals. + """ + yargilama_dairesi: DaireEnum = Field("ALL", description=""" + Audit chamber filter: + • ALL: All chambers (default) + • 1-8: Specific chamber number (1. Daire through 8. Daire) + Each chamber audits different types of public institutions. + """) + + karar_tarih_baslangic: Optional[str] = Field(None, description=""" + Decision start year for date range filtering. + Available years: 2012-2025. Format: 'YYYY' (e.g., '2020') + Use with karar_tarih_bitis for date range filtering. + """) + + karar_tarih_bitis: Optional[str] = Field(None, description=""" + Decision end year for date range filtering. + Available years: 2012-2025. Format: 'YYYY' (e.g., '2024') + Use with karar_tarih_baslangic for date range filtering. + """) + + ilam_no: Optional[str] = Field(None, description="Audit report number (İlam No, max 50 chars)") + + kamu_idaresi_turu: KamuIdaresiTuruEnum = Field("ALL", description=""" + Public administration type filter: + • ALL: All institutions (default) + • Genel Bütçe Kapsamındaki İdareler: General budget administrations + • Yüksek Öğretim Kurumları: Higher education institutions + • Belediyeler ve Bağlı İdareler: Municipalities and affiliates + • Other specific institution types + """) + + hesap_yili: Optional[str] = Field(None, description=""" + Account year filter (Hesap Yılı). + Available years: 2005, 2008-2023. Format: 'YYYY' (e.g., '2020') + Refers to the fiscal year being audited, not decision date. + """) + + web_karar_konusu: WebKararKonusuEnum = Field("ALL", description=""" + Decision subject category filter: + • ALL: All subjects (default) + • İhale Mevzuatı ile İlgili Kararlar: Procurement legislation + • Personel Mevzuatı ile İlgili Kararlar: Personnel legislation + • Vergi Resmi Harç ve Diğer Gelirlerle İlgili Kararlar: Tax and fee legislation + • Other specialized legal areas + """) + + web_karar_metni: Optional[str] = Field(None, description=""" + Content search within chamber decisions. + Searches decision text and audit findings. + Example: 'birim fiyat revize edilmemesi' + """) + + # DataTables pagination + start: int = Field(0, description="Starting record for pagination (0-based)") + length: int = Field(10, description="Number of records per page (1-100)") + +class DaireDecision(BaseModel): + """Single Daire decision entry from search results.""" + id: int = Field(..., description="Unique decision ID") + yargilama_dairesi: int = Field(..., description="Chamber number (1-8)") + karar_tarih: str = Field(..., description="Decision date in DD.MM.YYYY format") + karar_no: str = Field(..., description="Decision number") + ilam_no: Optional[str] = Field(None, description="Audit report number (may be null)") + madde_no: int = Field(..., description="Article/item number within the decision") + kamu_idaresi_turu: str = Field(..., description="Public administration type") + hesap_yili: int = Field(..., description="Account year being audited") + web_karar_konusu: str = Field(..., description="Decision subject category") + web_karar_metni: str = Field(..., description="Decision text/summary") + +class DaireSearchResponse(BaseModel): + """Response from Daire search endpoint.""" + decisions: List[DaireDecision] = Field(default_factory=list, description="List of matching chamber decisions") + total_records: int = Field(0, description="Total number of matching records") + total_filtered: int = Field(0, description="Number of records after filtering") + draw: int = Field(1, description="DataTables draw counter") + +# ============================================================================ +# Document Models +# ============================================================================ + +class SayistayDocumentMarkdown(BaseModel): + """ + Sayıştay decision document converted to Markdown format. + + Used for retrieving full text of decisions from any of the three + decision types (Genel Kurul, Temyiz Kurulu, Daire). + """ + decision_id: str = Field(..., description="Unique decision identifier") + decision_type: str = Field(..., description="Type of decision: 'genel_kurul', 'temyiz_kurulu', or 'daire'") + 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") + 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") \ No newline at end of file