File size: 2,291 Bytes
c0c4a30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import re
from dataclasses import dataclass
from typing import Sequence


def normalize_text(value: str) -> str:
    return re.sub(r"\s+", "", value.casefold())


@dataclass(frozen=True)
class Rule:
    rule_id: str
    primary_keyword: str
    aliases: tuple[str, ...]
    reply: str

    def search_terms(self) -> tuple[str, ...]:
        terms: list[str] = []
        for candidate in (self.primary_keyword, *self.aliases):
            cleaned = candidate.strip()
            if cleaned and cleaned not in terms:
                terms.append(cleaned)
        return tuple(terms)


@dataclass(frozen=True)
class MatchResult:
    transcript: str
    matched: bool
    rule_id: str | None = None
    matched_keyword: str | None = None
    reply: str | None = None
    match_position: int | None = None


class RuleEngine:
    def __init__(self, rules: Sequence[Rule]) -> None:
        self._rules = list(rules)

    @property
    def rules(self) -> list[Rule]:
        return list(self._rules)

    def update_rules(self, rules: Sequence[Rule]) -> None:
        self._rules = list(rules)

    def match(self, transcript: str) -> MatchResult:
        normalized_transcript = normalize_text(transcript)
        best_score: tuple[int, int] | None = None
        best_rule: Rule | None = None
        best_term: str | None = None

        for rule in self._rules:
            for term in rule.search_terms():
                normalized_term = normalize_text(term)
                if not normalized_term:
                    continue
                position = normalized_transcript.find(normalized_term)
                if position < 0:
                    continue
                score = (position, -len(normalized_term))
                if best_score is None or score < best_score:
                    best_score = score
                    best_rule = rule
                    best_term = term

        if best_rule is None or best_term is None or best_score is None:
            return MatchResult(transcript=transcript, matched=False)

        return MatchResult(
            transcript=transcript,
            matched=True,
            rule_id=best_rule.rule_id,
            matched_keyword=best_term,
            reply=best_rule.reply,
            match_position=best_score[0],
        )