| 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 | |