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="")
|