Code / libs/huleedu_nlp_shared/src/huleedu_nlp_shared/writing_issues/dialect.py
libs/huleedu_nlp_shared/src/huleedu_nlp_shared/writing_issues/dialect.py
1
"""Dialect variant policies for LanguageTool spelling evidence.
2
3
Purpose:
4
Provide a concrete paired-variant Morfologik policy that can suppress
5
dialect-valid spelling rows after probing accepted LanguageTool variants.
6
7
Relationships:
8
- `IssueLedgerPolicy` can inject `PairedMorfologikDialectPolicy`.
9
- NLP Service can implement `MorfologikVariantProbeProtocol` with its
10
LanguageTool client; tests can provide deterministic probes.
11
"""
12
13
from __future__ import annotations
14
15
from dataclasses import dataclass
16
from typing import Protocol
17
18
from pydantic import JsonValue
19
20
from huleedu_nlp_shared.writing_issues.models import IssueKind, WritingIssueRowV1
21
from huleedu_nlp_shared.writing_issues.protocols import DialectVariantDecision
22
23
24
class MorfologikVariantProbeProtocol(Protocol):
25
"""Probe whether another LT language variant accepts the same span."""
26
27
def accepts_span(
28
self,
29
*,
30
source_text: str,
31
issue: WritingIssueRowV1,
32
language: str,
33
) -> bool:
34
"""Return True when the variant does not flag the span as misspelled."""
35
36
37
@dataclass(frozen=True, slots=True)
38
class PairedMorfologikDialectPolicy:
39
"""Suppress Morfologik false positives accepted by paired variants."""
40
41
probe: MorfologikVariantProbeProtocol
42
checked_variants: tuple[str, ...]
43
44
def evaluate(
45
self,
46
*,
47
issue: WritingIssueRowV1,
48
source_text: str,
49
) -> DialectVariantDecision:
50
"""Return a dialect suppression decision for Morfologik spelling rows."""
51
52
if not _is_morfologik_spelling_issue(issue):
53
checked_variants: list[JsonValue] = []
54
checked_variants.extend(self.checked_variants)
55
return DialectVariantDecision(
56
suppress=False,
57
metadata={
58
"dialect_policy": "paired_morfologik",
59
"checked_variants": checked_variants,
60
"span_text": issue.span_text,
61
},
62
)
63
64
actually_checked: list[JsonValue] = []
65
for language in self.checked_variants:
66
actually_checked.append(language)
67
if self.probe.accepts_span(
68
source_text=source_text,
69
issue=issue,
70
language=language,
71
):
72
return DialectVariantDecision(
73
suppress=True,
74
reason="dialect_variant_accepted",
75
metadata={
76
"dialect_policy": "paired_morfologik",
77
"original_rule": issue.rule_id or "",
78
"original_language": str(issue.source_metadata.get("language", "")),
79
"checked_variants": actually_checked,
80
"accepted_variant": language,
81
"span_text": issue.span_text,
82
"offset": issue.offset,
83
"end_offset": issue.end_offset,
84
"issue_id": issue.issue_id,
85
"source_text_sha256": issue.source_text_sha256,
86
},
87
)
88
89
return DialectVariantDecision(
90
suppress=False,
91
metadata={
92
"dialect_policy": "paired_morfologik",
93
"original_rule": issue.rule_id or "",
94
"checked_variants": actually_checked,
95
"span_text": issue.span_text,
96
"offset": issue.offset,
97
"end_offset": issue.end_offset,
98
"issue_id": issue.issue_id,
99
},
100
)
101
102
103
def _is_morfologik_spelling_issue(issue: WritingIssueRowV1) -> bool:
104
return (
105
issue.issue_kind == IssueKind.SPELLING
106
and issue.rule_id is not None
107
and "MORFOLOGIK" in issue.rule_id.upper()
108
)