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