File size: 3,906 Bytes
921d377
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
78
79
80
81
82
83
84
85
86
87
88
"""
Intent classifier — maps free-text viewer input to an ``intent_code``.

Phase 1 (this batch) is a lightweight keyword / regex classifier.
Good enough for deterministic tests and the bootstrap demo. Phase
2 (later sprints) swaps in an LLM classifier behind the same
``classify_intent`` function signature — call sites never change.

Design rule: this module never decides policy. It only LABELS
input. The decision engine (``decision.py``) does the allow /
block / soft-refuse call using the label plus the active profile.
"""
from __future__ import annotations

import re
from dataclasses import dataclass
from typing import List, Optional, Pattern, Tuple


@dataclass(frozen=True)
class IntentMatch:
    """Result of classifying a single input."""

    intent_code: str
    confidence: float     # 0..1, heuristic
    matched_pattern: str  # debug aid — which rule fired


# Pattern tuples: (intent_code, regex). First match wins. Order
# matters — more specific patterns first, most general last.
#
# Intent codes are shared with ``ix_intent_map`` and the policy
# profiles' ``allowed_intents`` / ``blocked_intents`` fields.
_RULES: List[Tuple[str, Pattern[str]]] = [
    # --- Safety / hard blocks (flagged as-is; policy decides) ---
    ("minor_reference", re.compile(r"\b(child|minor|under\s*1[0-7]|kid)\b", re.I)),
    ("violence_request", re.compile(r"\b(hurt|kill|punch|stab|attack)\b", re.I)),
    ("non_consent_scenario", re.compile(r"\b(force|rape|unwilling)\b", re.I)),

    # --- Explicit (mature-only) ---
    ("explicit_request", re.compile(
        r"\b(pussy|dick|cock|boob|nude|naked|undress|strip)\b", re.I
    )),

    # --- Flirty / social-romantic ---
    ("flirt", re.compile(r"\b(baby|babe|cutie|honey|gorgeous|beautiful)\b", re.I)),
    ("tease", re.compile(r"\b(tease|lick lips|wink|smirk)\b", re.I)),
    ("compliment", re.compile(r"\b(you('?re| are) (cute|pretty|hot|sweet|amazing))\b", re.I)),

    # --- Education / language ---
    ("request_translation", re.compile(r"\b(how do you say|what does .* mean|translate)\b", re.I)),
    ("pronounce_request", re.compile(r"\b(how do I pronounce|say that again|pronunciation)\b", re.I)),
    ("request_hint", re.compile(r"\b(hint|clue|help me with|i('?m| am) stuck)\b", re.I)),
    ("skip_topic", re.compile(r"\b(skip|next topic|move on)\b", re.I)),
    ("answer_attempt", re.compile(
        r"\b(the answer is|i think it'?s|my guess|it'?s)\b", re.I
    )),
    ("quiz_response", re.compile(r"\b(option\s+[abcd]|answer\s+[abcd])\b", re.I)),
    ("request_example", re.compile(r"\b(give me an example|show me|for instance)\b", re.I)),
    ("vocabulary_lookup", re.compile(r"\b(define|meaning of|what is a )\b", re.I)),

    # --- General social ---
    ("ask_personal", re.compile(r"\b(who are you|what('?s| is) your name|tell me about yourself)\b", re.I)),
    ("request_photo_safe", re.compile(r"\b(show me (your|a) (face|smile|outfit))\b", re.I)),

    # --- Greetings / low-risk general fallback ---
    ("greeting", re.compile(r"\b(hi|hello|hey|morning|evening|yo)\b", re.I)),
    ("question_about_topic", re.compile(r"\?\s*$")),
]


def classify_intent(text: str) -> IntentMatch:
    """Classify one user utterance. Always returns an IntentMatch —
    unknown input maps to ``intent_code='unknown'`` with 0.0
    confidence. The decision engine then decides what to do with
    ``unknown`` (typically route to a default node).
    """
    if not text or not text.strip():
        return IntentMatch(intent_code="empty", confidence=0.0, matched_pattern="")
    for code, pattern in _RULES:
        if pattern.search(text):
            return IntentMatch(
                intent_code=code,
                confidence=0.7,  # regex rules are deliberately mid-confidence
                matched_pattern=pattern.pattern,
            )
    return IntentMatch(intent_code="unknown", confidence=0.0, matched_pattern="")