| """ |
| 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 |
| matched_pattern: str |
|
|
|
|
| |
| |
| |
| |
| |
| _RULES: List[Tuple[str, Pattern[str]]] = [ |
| |
| ("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_request", re.compile( |
| r"\b(pussy|dick|cock|boob|nude|naked|undress|strip)\b", re.I |
| )), |
|
|
| |
| ("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)), |
|
|
| |
| ("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)), |
|
|
| |
| ("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)), |
|
|
| |
| ("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, |
| matched_pattern=pattern.pattern, |
| ) |
| return IntentMatch(intent_code="unknown", confidence=0.0, matched_pattern="") |
|
|