Spaces:
Sleeping
Sleeping
nedaktov-ops
phase-1: CI workflow, ruff linting, keepalive alerting, VITE_API_URL, pre-commit hooks
f3e893e | """Lightweight legal relation extraction for EU directive text. | |
| Identifies: | |
| - Obligations (shall, must, required to) | |
| - Rights (has the right to, may, entitled to) | |
| - Prohibitions (shall not, prohibited, not permitted) | |
| - Conditions (provided that, subject to, where) | |
| - Legal actors (employer, controller, member state, etc.) | |
| """ | |
| import re | |
| OBLIGATION_PATTERNS = [ | |
| (r"(shall|must)\s+(ensure|take|establish|implement|provide|report|disclose|notify|adopt|designate|maintain)", "positive_obligation"), | |
| (r"(shall|must)\s+[a-z]+\s+and\s+(shall|must)", "compound_obligation"), | |
| (r"has\s+(?:the\s+)?duty\s+to", "positive_obligation"), | |
| (r"is\s+responsible\s+for", "responsibility"), | |
| (r"is\s+required\s+to", "requirement"), | |
| (r"shall\s+be\s+(?:responsible|accountable|liable)\s+for", "accountability"), | |
| (r"(obligation|duty|responsibility)\s+(?:to|of|shall|under)", "obligation_statement"), | |
| ] | |
| PROHIBITION_PATTERNS = [ | |
| (r"shall\s+not\s+(permit|allow|use|disclose|process|transfer|discriminate)", "prohibition"), | |
| (r"prohibited\s+from", "prohibition"), | |
| (r"not\s+permitted\s+to", "prohibition"), | |
| (r"may\s+not\s+(use|disclose|process|transfer|discriminate)", "restriction"), | |
| (r"shall\s+not\s+be\s+(construed|interpreted)\s+as", "non_application"), | |
| (r"no\s+(person|employer|undertaking|organisation)\s+(shall|may)", "prohibition"), | |
| ] | |
| RIGHT_PATTERNS = [ | |
| (r"has\s+(?:the\s+)?right\s+to", "right"), | |
| (r"is\s+entitled\s+to", "entitlement"), | |
| (r"may\s+(?:request|receive|access|obtain|submit|appeal)", "right"), | |
| (r"have\s+the\s+right\s+to", "right"), | |
| (r"(?:right|entitlement)\s+(?:of|to|under)", "right_statement"), | |
| (r"shall\s+have\s+the\s+right\s+to", "right"), | |
| ] | |
| CONDITION_PATTERNS = [ | |
| (r"provided\s+that", "condition"), | |
| (r"subject\s+to", "condition"), | |
| (r"where\s+the\s+(?:employer|controller|processor|undertaking|member\s+state)", "condition"), | |
| (r"unless\s+", "exception"), | |
| (r"except\s+where", "exception"), | |
| (r"in\s+the\s+event\s+that", "condition"), | |
| (r"in\s+accordance\s+with", "compliance_condition"), | |
| ] | |
| ACTOR_PATTERNS = [ | |
| r"(employer|employee|worker|applicant|job\s+seeker)", | |
| r"(controller|processor|data\s+subject)", | |
| r"(member\s+state|national\s+authority|competent\s+authority)", | |
| r"(commission|council|parliament|european\s+parliament)", | |
| r"(undertaking|company|enterprise|organization)", | |
| ] | |
| DEONTIC_PATTERNS = { | |
| "shall", "must", "required", "obliged", "duty", "duties", | |
| "responsible", "liability", "sanction", "penalty", "breach", | |
| "prohibited", "not permitted", "shall ensure", "shall take", | |
| "shall establish", "shall implement", "shall report", | |
| "obligation", "obligations", "responsibility", "responsibilities", | |
| "mandatory", "compliance", "compliant", | |
| } | |
| def extract_legal_relations(text: str) -> dict: | |
| """Extract legal relations from text chunks. | |
| Args: | |
| text: The text content of a chunk. | |
| Returns: | |
| Dict with keys: obligations, prohibitions, rights, conditions, actors | |
| """ | |
| relations = { | |
| "obligations": [], | |
| "prohibitions": [], | |
| "rights": [], | |
| "conditions": [], | |
| "actors": [], | |
| } | |
| text_lower = text.lower() | |
| def _extract_with_context(patterns, text_lower, text, context_window=60): | |
| results = [] | |
| for pattern, rel_type in patterns: | |
| for match in re.finditer(pattern, text_lower): | |
| start = max(0, match.start() - context_window) | |
| end = min(len(text), match.end() + context_window) | |
| context = text[start:end] | |
| results.append({ | |
| "type": rel_type, | |
| "matched_text": match.group(0), | |
| "context": context.strip(), | |
| }) | |
| return results | |
| relations["obligations"] = _extract_with_context(OBLIGATION_PATTERNS, text_lower, text) | |
| relations["prohibitions"] = _extract_with_context(PROHIBITION_PATTERNS, text_lower, text, 50) | |
| relations["rights"] = _extract_with_context(RIGHT_PATTERNS, text_lower, text, 50) | |
| relations["conditions"] = _extract_with_context(CONDITION_PATTERNS, text_lower, text, 40) | |
| # Extract actors | |
| seen_actors = set() | |
| for pattern in ACTOR_PATTERNS: | |
| for match in re.finditer(pattern, text_lower): | |
| actor = match.group(0).strip() | |
| if actor not in seen_actors: | |
| seen_actors.add(actor) | |
| relations["actors"].append(actor) | |
| return relations | |
| def has_deontic_language(text: str) -> bool: | |
| """Check if text contains deontic (obligation) language.""" | |
| text_lower = text.lower() | |
| return any(kw in text_lower for kw in DEONTIC_PATTERNS) | |
| def count_deontic_density(text: str) -> int: | |
| """Count the number of deontic language occurrences in text.""" | |
| text_lower = text.lower() | |
| return sum(text_lower.count(kw) for kw in DEONTIC_PATTERNS) | |
| def summarize_relations(relations: dict) -> str: | |
| """Create a human-readable summary of legal relations for prompt injection. | |
| Args: | |
| relations: Output from extract_legal_relations() | |
| Returns: | |
| A compact text summary of the legal relations found. | |
| """ | |
| parts = [] | |
| if relations["obligations"]: | |
| parts.append(f"OBLIGATIONS ({len(relations['obligations'])} found):") | |
| seen_types = {} | |
| for obl in relations["obligations"]: | |
| t = obl["type"] | |
| if t not in seen_types: | |
| seen_types[t] = 0 | |
| seen_types[t] += 1 | |
| for t, count in sorted(seen_types.items(), key=lambda x: -x[1]): | |
| parts.append(f" - {t}: {count} instance(s)") | |
| if relations["prohibitions"]: | |
| parts.append(f"PROHIBITIONS ({len(relations['prohibitions'])} found):") | |
| for pro in relations["prohibitions"][:2]: | |
| ctx = pro["context"][:80] | |
| parts.append(f" - {pro['type']}: ...{ctx}...") | |
| if relations["rights"]: | |
| parts.append(f"RIGHTS ({len(relations['rights'])} found)") | |
| for right in relations["rights"][:2]: | |
| ctx = right["context"][:80] | |
| parts.append(f" - {right['type']}: ...{ctx}...") | |
| if relations["conditions"]: | |
| parts.append(f"CONDITIONS ({len(relations['conditions'])} found)") | |
| if relations["actors"]: | |
| parts.append(f"ACTORS: {', '.join(relations['actors'][:5])}") | |
| return "\n".join(parts) | |