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

View this file on Codeberg


  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
      )