from __future__ import annotations import re from typing import Any from utils import normalize_for_search MODALITY_TERMS = { "obligation": ( "zorundadir", "zorunda", "zorunludur", "zorunlu", "yukumludur", "yukumlu", "mecburdur", "mecbur", "gerekir", "sarttir", "aranir", "odevidir", "tamamlamak zorundadir", ), "permission": ("yapabilir", "verilebilir", "yararlanabilir", "atanabilir", "secilir", "kurulabilir", "acilabilir"), "prohibition": ("yapamaz", "olamaz", "verilemez", "atanamaz", "kullanamaz", "calisamaz"), "power": ("yetkilidir", "karar verir", "belirler", "tespit eder", "onaylar", "sunar", "uygulatir"), "definition": ("denir", "ifade eder", "kapsar", "olup", "turudur"), } CONDITION_MARKERS = ( "halinde", "takdirde", "kaydi ile", "kaydiyla", "sarti ile", "sartiyla", "olmak uzere", "bulunmasi halinde", "basarmalari halinde", "tamamlamalari halinde", ) EXCEPTION_MARKERS = ( "ancak", "haric", "disinda", "saklidir", "sakli kalmak", "bunun disinda", "istisna", ) TEMPORAL_MARKERS = ( "yil", "ay", "gun", "yariyil", "donem", "sure", "suresi", "hafta", "en cok", "en az", "azami", ) ACTOR_PATTERNS = { "Cumhurbaşkanı": ("cumhurbaskani",), "Yükseköğretim Kurulu": ("yuksekogretim kurulu", "yok"), "Üniversitelerarası Kurul": ("universitelerarasi kurul",), "Milli Eğitim Bakanlığı": ("milli egitim bakanligi",), "rektör": ("rektor",), "dekan": ("dekan",), "senato": ("senato",), "üniversite yönetim kurulu": ("universite yonetim kurulu",), "fakülte kurulu": ("fakulte kurulu",), "fakülte yönetim kurulu": ("fakulte yonetim kurulu",), "üniversite": ("universite", "universiteler"), "fakülte": ("fakulte", "fakulteler"), "enstitü": ("enstitu", "enstituler"), "yüksekokul": ("yuksekokul", "yuksekokullar"), "meslek yüksekokulu": ("meslek yuksekokulu",), "araştırma ve uygulama merkezi": ( "arastirma ve uygulama merkezi", "arastirma uygulama merkezi", "arastirma ve uygulama merkezleri", "arastirma uygulama merkezleri", ), "öğrenci": ("ogrenci", "ogrenciler"), "öğretim elemanı": ("ogretim elemani", "ogretim elemanlari"), "öğretim üyesi": ("ogretim uyesi", "ogretim uyeleri"), "araştırma görevlisi": ("arastirma gorevlisi", "arastirma gorevlileri"), "öğretim görevlisi": ("ogretim gorevlisi", "ogretim gorevlileri"), "doçent": ("docent", "docentlik"), "profesör": ("profesor", "profesorluk"), } ACTION_PATTERNS = { "kurmak": ("kurulur", "kurulabilir", "kurulmasi", "kurma"), "açmak": ("acilir", "acilabilir", "acilmasi", "acma"), "kapatmak": ("kapatilir", "kapatilmasi", "kapatma"), "atamak": ("atanir", "atanabilir", "atanmasi", "atama"), "seçmek": ("secilir", "secer", "secim", "secilmesi"), "oluşmak": ("olusur", "oluşur", "olusur", "uyeden", "üyeden", "toplam"), "sınıflandırmak": ("siniflandirir", "siniflandirilir", "siniflandirma", "siniflari"), "belirlemek": ("belirler", "belirlenir", "belirlemek", "tespit eder", "tespit etmek"), "karar almak": ("karar almak", "karar verir", "karara baglamak"), "başkanlık etmek": ("baskanlik etmek", "baskanlik"), "rapor vermek": ("rapor vermek", "rapor verir"), "sunmak": ("sunmak", "sunar"), "denetlemek": ("denetlemek", "denetim", "gozetim"), "düzenlemek": ("duzenlemek", "duzenler", "duzenlenir"), "tamamlamak": ("tamamlamak", "tamamlamalari", "tamamlar"), "almak": ("alir", "alma", "almak"), "vermek": ("verilir", "vermek", "verir"), "eğitim vermek": ("egitim vermek", "egitim verir", "egitim veril", "egitim sun"), "yetiştirmek": ("yetistirir", "yetistiren", "yetistirmek", "yetistiril"), "araştırma yapmak": ( "arastirma yapmak", "arastirma ve uygulama yapmak", "arastirma yapar", "arastirmak", ), "basvurmak": ("basvuru", "basvurur", "basvurabilir", "muracaat"), "itiraz etmek": ("itiraz", "itiraz eder", "itiraz edebilir"), "ertelemek": ("ertelenir", "ertelenebilir", "erteleme"), "savunmak": ("savunma", "savunmasi", "savunmaya"), "devam etmek": ("devam", "devam eder", "devam zorunlulugu"), "odemek": ("odenir", "odeme", "ucret", "aylik"), "onaylamak": ("onaylar", "onaylanir", "uygun gorur", "uygun gormesi"), "bildirmek": ("bildirilir", "teblig", "ilan edilir", "duyurulur"), } OBJECT_HINTS = ( "on lisans", "onlisans", "diploma", "gorev", "yetki", "sorumluluk", "kurul", "fakulte", "enstitu", "yuksekokul", "universite", "ogrenci", "ogretim elemani", "program", "kredi", "uye", "uyeden", "kurul", "ceza", "unvan", "kadrosu", "rapor", "butce", "yonetmelik", "yonerge", "tez", "savunma", "tez savunmasi", "uzaktan egitim", "devam", "lisansustu", "kayit", "izin", "basvuru", "itiraz", "ders", "ucret", "odeme", "sinav", "takvim", "karar", "egitim", "kurum", "alt duzey kurum", "arastirma", "uygulama merkezi", ) PROCEDURE_MARKERS = ( "basvuru", "basvurur", "teklif", "onerisi", "oneri", "onay", "karar", "secilir", "atanir", "kurulur", "acilir", "belirlenir", "tespit", "ilan", "bildirilir", ) REMEDY_MARKERS = ("itiraz", "dava", "basvuru", "yeniden inceleme", "sikayet") SANCTION_MARKERS = ( "ceza", "yaptirim", "uyarma", "kinama", "uzaklastirma", "cikarma", "kesilir", "iptal", "kapatilir", "ilisigi kesilir", "mahrumiyet", ) REQUIRED_DOCUMENT_MARKERS = ( "dilekce", "belge", "form", "rapor", "tutanak", "tez", "proje", "transkript", "karar", "basvuru evraki", ) DECISION_MARKERS = ( "karar verir", "karara baglar", "onaylar", "uygun gorur", "belirler", "tespit eder", "kabul eder", "reddeder", ) NOTIFICATION_MARKERS = ("bildirilir", "teblig", "teblig edilir", "ilan edilir", "duyurulur") AUTHORITY_ACTIONS = {"atamak", "secmek", "belirlemek", "karar almak", "onaylamak", "kurmak", "acmak", "kapatmak"} KG_RELATION_FIELDS = { "competent_authority": "hasCompetentAuthority", "responsible_party": "hasResponsibleParty", "beneficiary": "hasBeneficiary", "burdened_party": "hasBurdenedParty", "regulated_entity": "regulates", "procedure_step": "hasProcedureStep", "required_document": "requiresDocument", "sanction": "hasSanction", "remedy": "hasRemedy", "temporal_rule": "hasTemporalRule", "quantitative_rule": "hasQuantitativeRule", } def extract_normative_frame(clause_text: str, inherited_actors: list[Any] | None = None) -> dict[str, Any]: """Extract a deterministic normative semantic frame from a clause. This is intentionally rule-based for v0.5. It does not pretend to be a full legal parser. Its job is to create explicit machine-readable hooks for retrieval: actor, action, object, condition, exception, temporal constraint and modality. """ text = " ".join(str(clause_text or "").split()) normalized = normalize_for_search(text) actors = _merge_contextual_actors(_extract_actors(normalized, []), inherited_actors or []) actions = _extract_actions(normalized) objects = _extract_objects(normalized) condition = _extract_marked_span(text, CONDITION_MARKERS) exception = _extract_marked_span(text, EXCEPTION_MARKERS) temporal = _extract_temporal(text) modality = _extract_modality(normalized) quantitative = _extract_quantitative_rules(text) norm_category = _norm_category(normalized, modality, actions, condition, exception, temporal) competent_authority = _extract_competent_authority(normalized, actors, actions, modality) beneficiaries = _extract_beneficiaries(normalized, actors) burdened_parties = _extract_burdened_parties(normalized, actors, modality) regulated_entities = _extract_regulated_entities(normalized, actors, objects) procedure_steps = _extract_procedure_steps(text) required_documents = _extract_required_documents(normalized) sanctions = _extract_marked_terms(text, SANCTION_MARKERS) remedies = _extract_marked_terms(text, REMEDY_MARKERS) frame = { "actor": actors, "action": actions, "object": objects, "definition_term": _definition_terms_from_clause(text), "recipient": [], "condition": condition, "exception": exception, "temporal_constraint": temporal, "temporal_rule": temporal, "quantitative_rule": quantitative, "threshold": quantitative, "modality": modality, "deontic_modality": modality, "norm_category": norm_category, "competent_authority": competent_authority, "responsible_party": _unique(competent_authority + actors[:3]), "beneficiary": beneficiaries, "burdened_party": burdened_parties, "regulated_entity": regulated_entities, "applicability_scope": _extract_applicability_scope(text), "eligibility_criteria": _extract_eligibility_criteria(text, condition), "procedure_step": procedure_steps, "required_document": required_documents, "approval_chain": _extract_approval_chain(text, competent_authority), "decision_point": _extract_marked_span(text, DECISION_MARKERS), "notification_requirement": _extract_marked_span(text, NOTIFICATION_MARKERS), "sanction": sanctions, "remedy": remedies, "related_concept": _extract_related_concepts(normalized, objects), "kg_relations": [], "legal_effect": text, "evidence_terms": _evidence_terms(normalized), "confidence": _frame_confidence( actors, actions, objects, condition, exception, temporal, modality, competent_authority, beneficiaries, procedure_steps, sanctions, remedies, quantitative, ), } frame["kg_relations"] = _kg_relations(frame) return frame def extract_question_frame(question: str) -> dict[str, Any]: normalized = normalize_for_search(question) actors = _extract_actors(normalized, []) actions = _extract_actions(normalized) objects = _extract_objects(normalized) condition = _question_condition_need(normalized) exception = _question_exception_need(normalized) temporal = _question_temporal_need(normalized) modality = _question_modality(normalized) requested_categories = _question_requested_categories(normalized) return { "target_entity": _question_target_entity(normalized), "actor": actors, "action": actions, "object": objects, "definition_term": _definition_terms_from_question(question), "recipient": [], "condition": condition, "exception": exception, "temporal_constraint": temporal, "temporal_rule": temporal, "quantitative_rule": _question_quantitative_need(normalized), "threshold": _question_quantitative_need(normalized), "modality": modality, "deontic_modality": modality, "norm_category": _question_norm_categories(normalized, modality), "requested_categories": requested_categories, "competent_authority": actors if "competent_authority" in requested_categories else [], "responsible_party": actors if "responsible_party" in requested_categories else [], "beneficiary": actors if "beneficiary" in requested_categories else [], "burdened_party": actors if modality in {"obligation", "prohibition"} else [], "regulated_entity": _unique(actors + objects), "applicability_scope": ["scope_requested"] if "scope" in requested_categories else [], "eligibility_criteria": ["eligibility_requested"] if "eligibility" in requested_categories else [], "procedure_step": ["procedure_requested"] if "procedure" in requested_categories else [], "required_document": ["document_requested"] if "required_document" in requested_categories else [], "decision_point": ["decision_requested"] if "decision_point" in requested_categories else [], "notification_requirement": ["notification_requested"] if "notification" in requested_categories else [], "sanction": ["sanction_requested"] if "sanction" in requested_categories else [], "remedy": ["remedy_requested"] if "remedy" in requested_categories else [], "related_concept": _extract_related_concepts(normalized, objects), "normalized_question": normalized, } def _question_target_entity(normalized: str) -> list[str]: """Extract the office/entity whose own rule is requested, not nearby actors.""" patterns = ( r"^(.{2,80}?)\s+(?:nasil\s+atanir|kim\s+atar|kim\s+tarafindan\s+atanir)", r"^(.{2,80}?)\s+(?:gorevleri|yetkileri|sorumluluklari)\b", ) noise = {"bir", "bu", "sayili", "kanuna", "gore", "universitede"} for pattern in patterns: match = re.search(pattern, normalized) if not match: continue tokens = [token for token in match.group(1).split() if token not in noise and not token.isdigit()] if tokens: return [" ".join(tokens[-3:])] return [] def semantic_role_match_score(question_frame: dict[str, Any], clause_frame: dict[str, Any]) -> dict[str, Any]: """Return normalized SRM component scores and weighted total in [0, 1].""" actor = _overlap_score(question_frame.get("actor", []), clause_frame.get("actor", [])) action = _overlap_score(question_frame.get("action", []), clause_frame.get("action", [])) definition_term = _strict_overlap_score(question_frame.get("definition_term", []), clause_frame.get("definition_term", [])) obj = _overlap_score(question_frame.get("object", []), clause_frame.get("object", [])) condition = _need_score(question_frame.get("condition", []), clause_frame.get("condition", [])) exception = _need_score(question_frame.get("exception", []), clause_frame.get("exception", [])) temporal = _need_score(question_frame.get("temporal_constraint", []), clause_frame.get("temporal_constraint", [])) modality = _modality_score(question_frame.get("modality", ""), clause_frame.get("modality", "")) kg = normative_kg_alignment_score(question_frame, clause_frame) weights = { "actor": 0.14, "action": 0.14, "definition_term": 0.16, "object": 0.10, "condition": 0.10, "exception": 0.07, "temporal_constraint": 0.05, "modality": 0.05, "kg_alignment": 0.19, } total = ( weights["actor"] * actor + weights["action"] * action + weights["definition_term"] * definition_term + weights["object"] * obj + weights["condition"] * condition + weights["exception"] * exception + weights["temporal_constraint"] * temporal + weights["modality"] * modality + weights["kg_alignment"] * kg.get("total", 0.0) ) return { "total": round(total, 4), "actor": round(actor, 4), "action": round(action, 4), "definition_term": round(definition_term, 4), "object": round(obj, 4), "condition": round(condition, 4), "exception": round(exception, 4), "temporal_constraint": round(temporal, 4), "modality": round(modality, 4), "kg_alignment": kg, "weights": weights, } def enrich_normative_frame( source_text: str, frame: dict[str, Any] | None = None, inherited_actors: list[Any] | None = None, ) -> dict[str, Any]: """Backfill the expanded KG frame for older ontology files at runtime.""" current = dict(frame or {}) if current.get("norm_category") and current.get("kg_relations") is not None: current["deontic_modality"] = current.get("deontic_modality") or current.get("modality", "") current["temporal_rule"] = current.get("temporal_rule") or current.get("temporal_constraint") or [] current["threshold"] = current.get("threshold") or current.get("quantitative_rule") or [] return current extracted = extract_normative_frame(source_text, inherited_actors or []) for key, value in extracted.items(): if _is_empty_frame_value(current.get(key)): current[key] = value elif isinstance(current.get(key), list) and isinstance(value, list): current[key] = _unique([*current.get(key, []), *value]) elif isinstance(current.get(key), dict) and isinstance(value, dict): merged = dict(value) merged.update(current.get(key, {})) current[key] = merged current["deontic_modality"] = current.get("deontic_modality") or current.get("modality") or extracted.get("deontic_modality", "") current["temporal_rule"] = current.get("temporal_rule") or current.get("temporal_constraint") or [] current["threshold"] = current.get("threshold") or current.get("quantitative_rule") or [] current["norm_category"] = current.get("norm_category") or extracted.get("norm_category", "norm") current["kg_relations"] = _kg_relations(current) current["confidence"] = max(float(current.get("confidence", 0.0) or 0.0), float(extracted.get("confidence", 0.0) or 0.0)) return current def build_normative_knowledge_unit( source_text: str, frame: dict[str, Any] | None, source_ref: dict[str, Any] | None = None, ) -> dict[str, Any]: frame = dict(frame or {}) if not frame.get("norm_category"): frame = enrich_normative_frame(source_text, frame) if not frame.get("kg_relations"): frame["kg_relations"] = _kg_relations(frame) return { "unit_schema": "MCKF-NormativeKnowledgeUnit-v0.9", "source_ref": source_ref or {}, "norm_category": frame.get("norm_category", "norm"), "deontic_modality": frame.get("deontic_modality") or frame.get("modality", ""), "actors": frame.get("actor", []), "competent_authority": frame.get("competent_authority", []), "responsible_party": frame.get("responsible_party", []), "beneficiary": frame.get("beneficiary", []), "burdened_party": frame.get("burdened_party", []), "regulated_entity": frame.get("regulated_entity", []), "actions": frame.get("action", []), "objects": frame.get("object", []), "conditions": frame.get("condition", []), "exceptions": frame.get("exception", []), "temporal_rules": frame.get("temporal_rule", []), "quantitative_rules": frame.get("quantitative_rule", []), "eligibility_criteria": frame.get("eligibility_criteria", []), "procedure_steps": frame.get("procedure_step", []), "required_documents": frame.get("required_document", []), "approval_chain": frame.get("approval_chain", []), "decision_points": frame.get("decision_point", []), "notification_requirements": frame.get("notification_requirement", []), "sanctions": frame.get("sanction", []), "remedies": frame.get("remedy", []), "related_concepts": frame.get("related_concept", []), "kg_relations": frame.get("kg_relations", []), "legal_effect": frame.get("legal_effect", source_text), "confidence": frame.get("confidence", 0.0), } def normative_kg_alignment_score(question_frame: dict[str, Any], clause_frame: dict[str, Any]) -> dict[str, Any]: requested = set(question_frame.get("requested_categories", []) or []) category = _category_match_score(question_frame.get("norm_category", []), clause_frame.get("norm_category", "")) authority = _kg_need_or_overlap("competent_authority", question_frame, clause_frame, requested) responsible = _kg_need_or_overlap("responsible_party", question_frame, clause_frame, requested) beneficiary = _kg_need_or_overlap("beneficiary", question_frame, clause_frame, requested) burdened = _kg_need_or_overlap("burdened_party", question_frame, clause_frame, requested) scope = _kg_need_or_overlap("applicability_scope", question_frame, clause_frame, requested) eligibility = _kg_need_or_overlap("eligibility_criteria", question_frame, clause_frame, requested) procedure = _kg_need_or_overlap("procedure_step", question_frame, clause_frame, requested) required_document = _kg_need_or_overlap("required_document", question_frame, clause_frame, requested) decision = _kg_need_or_overlap("decision_point", question_frame, clause_frame, requested) notification = _kg_need_or_overlap("notification_requirement", question_frame, clause_frame, requested) sanction = _kg_need_or_overlap("sanction", question_frame, clause_frame, requested) remedy = _kg_need_or_overlap("remedy", question_frame, clause_frame, requested) quantitative = _kg_need_or_overlap("quantitative_rule", question_frame, clause_frame, requested) related = _overlap_score(question_frame.get("related_concept", []), clause_frame.get("related_concept", [])) components = { "category": category, "authority": authority, "responsible": responsible, "beneficiary": beneficiary, "burdened": burdened, "scope": scope, "eligibility": eligibility, "procedure": procedure, "required_document": required_document, "decision": decision, "notification": notification, "sanction": sanction, "remedy": remedy, "quantitative": quantitative, "related_concept": related, } weights = { "category": 0.16, "authority": 0.10, "responsible": 0.06, "beneficiary": 0.06, "burdened": 0.05, "scope": 0.05, "eligibility": 0.08, "procedure": 0.10, "required_document": 0.06, "decision": 0.06, "notification": 0.04, "sanction": 0.06, "remedy": 0.06, "quantitative": 0.05, "related_concept": 0.05, } total = sum(weights[key] * components[key] for key in weights) return { "total": round(total, 4), **{key: round(value, 4) for key, value in components.items()}, } def _merge_contextual_actors(explicit_actors: list[str], inherited_actors: list[Any]) -> list[str]: """Carry clause-level subject into smaller evidence spans. Long legal clauses often state the governed entity once and then continue with semicolon-separated details. Evidence spans below that clause need the parent subject so composition/exception/duty questions are not misrouted to another institution merely because a subspan lacks the original noun phrase. """ inherited = [str(actor).replace("_", " ") for actor in inherited_actors if actor] return _unique(list(explicit_actors or []) + inherited[:4]) def _norm_category( normalized: str, modality: str, actions: list[str], condition: list[str], exception: list[str], temporal: list[str], ) -> str: if modality == "definition": return "definition" if any(marker in normalized for marker in SANCTION_MARKERS) or modality == "prohibition": return "sanction" if any(marker in normalized for marker in REMEDY_MARKERS): return "remedy" if any(marker in normalized for marker in PROCEDURE_MARKERS): return "procedure" if any(term in normalized for term in ("hak", "yararlan", "alabilir", "isteyebilir")): return "right" if modality == "obligation": return "obligation" if modality == "permission": return "permission" if modality == "power" or any(normalize_for_search(action) in {"atamak", "secmek", "belirlemek", "karar almak"} for action in actions): return "authority" if condition: return "condition" if exception: return "exception" if temporal: return "temporal" return "norm" def _extract_competent_authority(normalized: str, actors: list[str], actions: list[str], modality: str) -> list[str]: action_text = " ".join(normalize_for_search(action) for action in actions) has_authority_signal = modality == "power" or any(term in normalized for term in DECISION_MARKERS) or any( term in action_text for term in ("atamak", "secmek", "belirlemek", "karar", "onaylamak", "kurmak", "acmak", "kapatmak") ) if has_authority_signal: return actors[:4] return [] def _extract_beneficiaries(normalized: str, actors: list[str]) -> list[str]: if any(term in normalized for term in ("hak", "yararlan", "alabilir", "verilir", "odenir", "burs", "diploma")): return actors[:4] or _unique([obj for obj in OBJECT_HINTS if obj in normalized and obj in {"ogrenci", "ogretim elemani", "ogretim uyesi"}]) return [] def _extract_burdened_parties(normalized: str, actors: list[str], modality: str) -> list[str]: if modality in {"obligation", "prohibition"} or any(term in normalized for term in ("zorundadir", "yukumludur", "yapamaz", "gerekir")): return actors[:4] return [] def _extract_regulated_entities(normalized: str, actors: list[str], objects: list[str]) -> list[str]: entity_objects = [ obj for obj in objects if obj in { "ogrenci", "ogretim elemani", "ogretim uyesi", "universite", "fakulte", "enstitu", "program", "tez", "savunma", "uzaktan egitim", "devam", "ders", "sinav", } ] return _unique(actors[:3] + entity_objects[:5]) def _extract_applicability_scope(text: str) -> list[str]: normalized = normalize_for_search(text) markers = ("kapsar", "hakkinda", "uygulanir", "bu kanun", "bu madde", "ilgili", "dahil") if not any(marker in normalized for marker in markers): return [] return _extract_marked_span(text, markers) def _extract_eligibility_criteria(text: str, condition: list[str]) -> list[str]: normalized = normalize_for_search(text) markers = ("aranir", "gerekir", "sarttir", "basarmalari halinde", "tamamlamalari halinde", "hak kazan") criteria = list(condition or []) if any(marker in normalized for marker in markers): criteria.extend(_extract_marked_span(text, markers)) return _unique(criteria)[:5] def _extract_procedure_steps(text: str) -> list[str]: normalized = normalize_for_search(text) if not any(marker in normalized for marker in PROCEDURE_MARKERS): return [] return _extract_marked_span(text, PROCEDURE_MARKERS)[:5] def _extract_required_documents(normalized: str) -> list[str]: return _unique([marker for marker in REQUIRED_DOCUMENT_MARKERS if marker in normalized])[:6] def _extract_approval_chain(text: str, authorities: list[str]) -> list[str]: normalized = normalize_for_search(text) if not authorities and not any(marker in normalized for marker in ("onay", "teklif", "uygun gor", "karar")): return [] chain = list(authorities or []) for marker in ("teklif", "oneri", "onay", "karar", "uygun gor"): if marker in normalized: chain.append(marker) return _unique(chain)[:6] def _extract_marked_terms(text: str, markers: tuple[str, ...]) -> list[str]: normalized = normalize_for_search(text) if not any(marker in normalized for marker in markers): return [] spans = _extract_marked_span(text, markers) return spans[:4] if spans else [marker for marker in markers if marker in normalized][:4] def _extract_quantitative_rules(text: str) -> list[str]: patterns = [ r"(?:en\s+(?:az|cok|�ok)|azami|asgari)?\s*\d+\s*(?:yil|y�l|ay|gun|g�n|hafta|yariyil|yar�y�l|donem|d�nem|kredi|saat|uye|�ye|oran|%)", r"%\s*\d+", r"(?:salt|ucte iki|��te iki|yaridan fazla|yar�dan fazla|cogunluk|�o�unluk)", ] hits: list[str] = [] for pattern in patterns: hits.extend(match.group(0).strip() for match in re.finditer(pattern, text, flags=re.IGNORECASE)) return _unique(hits)[:6] def _extract_related_concepts(normalized: str, objects: list[str]) -> list[str]: concept_terms = [ "azami sure", "devam zorunlulugu", "tez savunmasi", "kayit dondurma", "basari sarti", "disiplin cezasi", "ek ders", "uzaktan egitim", "akademik takvim", "yonetmelik", "yonerge", "senato karari", "itiraz", "basvuru", ] return _unique(objects + [term for term in concept_terms if term in normalized])[:10] def _kg_relations(frame: dict[str, Any]) -> list[dict[str, str]]: relations: list[dict[str, str]] = [] for field, predicate in KG_RELATION_FIELDS.items(): for target in frame.get(field, []) or []: target_norm = normalize_for_search(str(target)) if target_norm: relations.append({"predicate": predicate, "target": str(target), "target_norm": target_norm}) return relations[:24] def _question_requested_categories(normalized: str) -> list[str]: categories = [] if any(term in normalized for term in ("siniflandir", "siniflari", "siniflardan")): categories.append("definition") if any(term in normalized for term in ("kim", "hangi makam", "hangi kurul", "kim karar", "kim atar", "kim belirler")): categories.extend(["competent_authority", "responsible_party", "decision_point"]) if any(term in normalized for term in ("hak", "yararlan", "kimler", "kime", "kimlere")): categories.append("beneficiary") if any(term in normalized for term in ("zorunlu", "yukumluluk", "sorumlu", "gerekir")): categories.append("burdened_party") if any(term in normalized for term in ("kapsam", "kimleri kapsar", "hangi durumda", "uygulanir")): categories.append("scope") if any(term in normalized for term in ("sart", "kosul", "aranir", "gerekir", "icin ne gerekir")): categories.append("eligibility") if any(term in normalized for term in ("nasil", "basvuru", "adim", "surec", "islem")): categories.append("procedure") if any(term in normalized for term in ("hangi belge", "belge gerekir", "evrak", "dokuman")): categories.append("required_document") if any(term in normalized for term in ("karar", "onay", "belirler", "tespit")): categories.append("decision_point") if any(term in normalized for term in ("bildirim", "teblig", "ilan", "duyuru")): categories.append("notification") if any(term in normalized for term in ("ceza", "yaptirim", "sonuc", "iptal", "kesilir")): categories.append("sanction") if any(term in normalized for term in ("itiraz", "dava", "basvuru yolu", "ne yapabilir")): categories.append("remedy") if any(term in normalized for term in ("kac", "ne kadar", "oran", "en az", "en cok", "azami", "asgari")): categories.append("quantitative") return _unique(categories) def _question_norm_categories(normalized: str, modality: str) -> list[str]: categories = [] if modality == "definition" or any(term in normalized for term in ("nedir", "tanim", "ne demek", "siniflandir", "siniflari")): categories.append("definition") if any(term in normalized for term in ("hak", "yararlan")): categories.append("right") if modality == "obligation": categories.append("obligation") if modality == "permission": categories.append("permission") if modality == "prohibition": categories.append("prohibition") if modality == "power" or any(term in normalized for term in ("kim", "yetki", "yetkili")): categories.append("authority") if any(term in normalized for term in ("nasil", "basvuru", "surec", "adim")): categories.append("procedure") if any(term in normalized for term in ("ceza", "yaptirim")): categories.append("sanction") if any(term in normalized for term in ("itiraz", "dava")): categories.append("remedy") if any(term in normalized for term in ("sart", "kosul", "gerekir")): categories.append("condition") return _unique(categories) def _question_quantitative_need(normalized: str) -> list[str]: if any(term in normalized for term in ("kac", "ne kadar", "oran", "en az", "en cok", "azami", "asgari", "sure", "suresi")): return ["quantitative_requested"] return [] def _category_match_score(query_categories: list[str], clause_category: str) -> float: if not query_categories: return 0.5 if not clause_category: return 0.0 clause = normalize_for_search(clause_category) query = {normalize_for_search(item) for item in query_categories} if clause in query: return 1.0 compatible = { ("authority", "procedure"), ("procedure", "authority"), ("right", "permission"), ("permission", "right"), ("condition", "eligibility"), ("remedy", "procedure"), } return 0.65 if any((item, clause) in compatible for item in query) else 0.0 def _kg_need_or_overlap(field: str, question_frame: dict[str, Any], clause_frame: dict[str, Any], requested: set[str]) -> float: q_values = question_frame.get(field, []) or [] c_values = clause_frame.get(field, []) or [] requested_alias = { "temporal_rule": "temporal", "quantitative_rule": "quantitative", "notification_requirement": "notification", "eligibility_criteria": "eligibility", }.get(field, field) if field in requested or requested_alias in requested: return 1.0 if c_values else 0.0 return _overlap_score(q_values, c_values) def _is_empty_frame_value(value: Any) -> bool: return value is None or value == "" or value == [] or value == {} def _definition_terms_from_clause(text: str) -> list[str]: cleaned = " ".join(str(text or "").split()) cleaned = re.sub(r"^[a-zçğıöşü]\)\s*", "", cleaned, flags=re.IGNORECASE) # Remove amendment notes before the actual defined term. while cleaned.startswith("("): next_text = re.sub(r"^\([^)]*\)\s*", "", cleaned) if next_text == cleaned: break cleaned = next_text match = re.search(r"^([^:]{2,80}):", cleaned) if not match: return [] term = re.sub(r"\([^)]*\)", " ", match.group(1)) term = re.sub(r"[^A-Za-zÇĞİÖŞÜçğıöşü0-9 ]+", " ", term).strip() return _unique([term, normalize_for_search(term).replace(" ", "")]) def _definition_terms_from_question(question: str) -> list[str]: normalized = normalize_for_search(question) terms = [] if "nedir" in normalized: before = normalized.split("nedir", 1)[0].strip() if before: terms.append(before) if "ne demek" in normalized: before = normalized.split("ne demek", 1)[0].strip() if before: terms.append(before) compact = normalized.replace(" ", "") if "onlisans" in compact: terms.extend(["on lisans", "onlisans"]) if "diploma" in normalized: terms.append("diploma") return _unique(terms) def _extract_actors(normalized: str, inherited_actors: list[Any]) -> list[str]: actors: list[str] = [] for label, patterns in ACTOR_PATTERNS.items(): if any(pattern in normalized for pattern in patterns): actors.append(label) if not actors: actors = [str(actor).replace("_", " ") for actor in inherited_actors[:3] if actor] return _unique(actors) def _extract_actions(normalized: str) -> list[str]: actions = [] for action, patterns in ACTION_PATTERNS.items(): if any(pattern in normalized for pattern in patterns): actions.append(action) return _unique(actions) def _extract_objects(normalized: str) -> list[str]: return _unique([obj for obj in OBJECT_HINTS if obj in normalized])[:8] def _extract_modality(normalized: str) -> str: if any(term in normalized for term in ("denir", "ifade eder", "nedir")): return "definition" if any(term in normalized for term in ("yuksekogretimdir", "kurumudur", "turudur", "unvanidir")): return "definition" for modality, terms in MODALITY_TERMS.items(): if any(term in normalized for term in terms): return modality if any(action in normalized for action in ("atanir", "secilir", "kurulur", "belirlenir")): return "power" return "statement" def _extract_marked_span(text: str, markers: tuple[str, ...]) -> list[str]: normalized = normalize_for_search(text) if not any(marker in normalized for marker in markers): return [] parts = re.split(r"(?<=[.;:])\s+", text) selected = [part.strip() for part in parts if any(marker in normalize_for_search(part) for marker in markers)] return selected[:3] or [text] def _extract_temporal(text: str) -> list[str]: normalized = normalize_for_search(text) if not any(marker in normalized for marker in TEMPORAL_MARKERS): return [] patterns = [ r"(?:en\s+(?:çok|cok|az)|azami)?\s*\d+\s*(?:yıl|yil|ay|gün|gun|yarıyıl|yariyil|dönem|donem|hafta)", r"(?:bir|iki|üç|uc|dört|dort|beş|bes|altı|alti|yedi|sekiz|dokuz|on)\s*(?:yıl|yil|ay|gün|gun|yarıyıl|yariyil|dönem|donem|hafta)", ] hits = [] for pattern in patterns: hits.extend(match.group(0).strip() for match in re.finditer(pattern, text, flags=re.IGNORECASE)) return _unique(hits)[:5] def _question_condition_need(normalized: str) -> list[str]: if any(term in normalized for term in ("hangi durumda", "hangi sart", "kosul", "sart", "ne zaman", "nasil")): return ["condition_requested"] return [] def _question_exception_need(normalized: str) -> list[str]: if any(term in normalized for term in ("istisna", "haric", "ancak", "sakli", "disinda")): return ["exception_requested"] return [] def _question_temporal_need(normalized: str) -> list[str]: if any(term in normalized for term in ("kac yil", "kac ay", "sure", "suresi", "ne kadar")): return ["temporal_requested"] return [] def _question_modality(normalized: str) -> str: if any(term in normalized for term in ( "zorunlu", "zorunda", "gerekir", "sart", "yukumlu", "mecbur", "odev", )): return "obligation" if any(term in normalized for term in ("yapabilir", "mümkün", "mumkun", "izin")): return "permission" if any(term in normalized for term in ("yasak", "yapamaz", "olamaz")): return "prohibition" if any(term in normalized for term in ("yetki", "kim", "belirler", "atar", "kurar")): return "power" if any(term in normalized for term in ("nedir", "ne demek", "tanim")): return "definition" return "" def _evidence_terms(normalized: str) -> list[str]: terms = [] for group in (CONDITION_MARKERS, EXCEPTION_MARKERS, TEMPORAL_MARKERS): terms.extend(marker for marker in group if marker in normalized) for modality, modality_terms in MODALITY_TERMS.items(): if any(term in normalized for term in modality_terms): terms.append(modality) return _unique(terms) def _frame_confidence( actors: list[str], actions: list[str], objects: list[str], condition: list[str], exception: list[str], temporal: list[str], modality: str, competent_authority: list[str] | None = None, beneficiaries: list[str] | None = None, procedure_steps: list[str] | None = None, sanctions: list[str] | None = None, remedies: list[str] | None = None, quantitative: list[str] | None = None, ) -> float: score = 0.1 score += 0.2 if actors else 0.0 score += 0.2 if actions else 0.0 score += 0.15 if objects else 0.0 score += 0.1 if condition else 0.0 score += 0.1 if exception else 0.0 score += 0.1 if temporal else 0.0 score += 0.15 if modality and modality != "statement" else 0.0 score += 0.08 if competent_authority else 0.0 score += 0.06 if beneficiaries else 0.0 score += 0.08 if procedure_steps else 0.0 score += 0.08 if sanctions else 0.0 score += 0.08 if remedies else 0.0 score += 0.06 if quantitative else 0.0 return round(min(score, 1.0), 3) def _strict_overlap_score(query_values: list[str], clause_values: list[str]) -> float: if not query_values: return 0.5 if not clause_values: return 0.0 q = {normalize_for_search(str(value)).replace(" ", "") for value in query_values if value} c = {normalize_for_search(str(value)).replace(" ", "") for value in clause_values if value} if not q: return 0.5 hits = sum(1 for item in q if item in c) return hits / max(1, len(q)) def _overlap_score(query_values: list[str], clause_values: list[str]) -> float: if not query_values: return 0.5 if not clause_values: return 0.0 q = {normalize_for_search(str(value)) for value in query_values if value} c = {normalize_for_search(str(value)) for value in clause_values if value} if not q: return 0.5 hits = 0 compact_c = {candidate.replace(" ", "") for candidate in c} for item in q: compact_item = item.replace(" ", "") if item in c or compact_item in compact_c or any(item in candidate or candidate in item or compact_item in candidate.replace(" ", "") or candidate.replace(" ", "") in compact_item for candidate in c): hits += 1 return hits / max(1, len(q)) def _need_score(query_need: list[str], clause_values: list[str]) -> float: if not query_need: return 0.5 return 1.0 if clause_values else 0.0 def _modality_score(query_modality: str, clause_modality: str) -> float: if not query_modality: return 0.5 if not clause_modality: return 0.0 if query_modality == clause_modality: return 1.0 compatible = { ("power", "permission"), ("permission", "power"), ("obligation", "power"), ("definition", "statement"), } return 0.6 if (query_modality, clause_modality) in compatible else 0.0 def _unique(values: list[str]) -> list[str]: seen = set() result = [] for value in values: key = normalize_for_search(str(value)) if not key or key in seen: continue seen.add(key) result.append(str(value)) return result