forked from Rockachopa/Timmy-time-dashboard
165 lines
4.6 KiB
Python
165 lines
4.6 KiB
Python
"""Natural Language Understanding — intent detection for voice commands.
|
|
|
|
Uses regex patterns and keyword matching to classify user utterances
|
|
into actionable intents. This is a lightweight, local-first NLU that
|
|
runs without any cloud API — just pattern matching.
|
|
|
|
Intents:
|
|
- chat: General conversation with Timmy
|
|
- status: Request system/agent status
|
|
- swarm: Swarm management commands
|
|
- task: Task creation/management
|
|
- help: Request help or list commands
|
|
- voice: Voice settings (volume, rate, etc.)
|
|
- code: Code modification / self-modify commands
|
|
- unknown: Unrecognized intent
|
|
"""
|
|
|
|
import logging
|
|
import re
|
|
from dataclasses import dataclass
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class Intent:
|
|
"""A classified user intent with confidence score and extracted entities."""
|
|
|
|
name: str
|
|
confidence: float # 0.0 to 1.0
|
|
entities: dict
|
|
raw_text: str
|
|
|
|
|
|
# ── Pattern definitions ─────────────────────────────────────────────────────
|
|
|
|
_PATTERNS: list[tuple[str, re.Pattern, float]] = [
|
|
# Status queries
|
|
(
|
|
"status",
|
|
re.compile(
|
|
r"\b(status|health|how are you|are you (running|online|alive)|check)\b",
|
|
re.IGNORECASE,
|
|
),
|
|
0.9,
|
|
),
|
|
# Swarm commands
|
|
(
|
|
"swarm",
|
|
re.compile(
|
|
r"\b(swarm|spawn|agents?|sub-?agents?|workers?)\b",
|
|
re.IGNORECASE,
|
|
),
|
|
0.85,
|
|
),
|
|
# Task commands
|
|
(
|
|
"task",
|
|
re.compile(
|
|
r"\b(task|assign|create task|new task|post task|bid)\b",
|
|
re.IGNORECASE,
|
|
),
|
|
0.85,
|
|
),
|
|
# Help
|
|
(
|
|
"help",
|
|
re.compile(
|
|
r"\b(help|commands?|what can you do|capabilities)\b",
|
|
re.IGNORECASE,
|
|
),
|
|
0.9,
|
|
),
|
|
# Voice settings
|
|
(
|
|
"voice",
|
|
re.compile(
|
|
r"\b(voice|speak|volume|rate|speed|louder|quieter|faster|slower|mute|unmute)\b",
|
|
re.IGNORECASE,
|
|
),
|
|
0.85,
|
|
),
|
|
# Code modification / self-modify
|
|
(
|
|
"code",
|
|
re.compile(
|
|
r"\b(modify|edit|change|update|fix|refactor|implement|patch)\s+(the\s+)?(code|file|function|class|module|source)\b"
|
|
r"|\bself[- ]?modify\b"
|
|
r"|\b(update|change|edit)\s+(your|the)\s+(code|source)\b",
|
|
re.IGNORECASE,
|
|
),
|
|
0.9,
|
|
),
|
|
]
|
|
|
|
# Keywords for entity extraction
|
|
_ENTITY_PATTERNS = {
|
|
"agent_name": re.compile(
|
|
r"(?:spawn|start)\s+(?:agent\s+)?(\w+)|(?:agent)\s+(\w+)", re.IGNORECASE
|
|
),
|
|
"task_description": re.compile(r"(?:task|assign)[:;]?\s+(.+)", re.IGNORECASE),
|
|
"number": re.compile(r"\b(\d+)\b"),
|
|
"target_file": re.compile(r"(?:in|file|modify)\s+(?:the\s+)?([/\w._-]+\.py)", re.IGNORECASE),
|
|
}
|
|
|
|
|
|
def detect_intent(text: str) -> Intent:
|
|
"""Classify a text utterance into an intent with entities.
|
|
|
|
Returns the highest-confidence matching intent, or 'chat' as the
|
|
default fallback (everything is a conversation with Timmy).
|
|
"""
|
|
text = text.strip()
|
|
if not text:
|
|
return Intent(name="unknown", confidence=0.0, entities={}, raw_text=text)
|
|
|
|
best_intent = "chat"
|
|
best_confidence = 0.5 # Default chat confidence
|
|
|
|
for intent_name, pattern, confidence in _PATTERNS:
|
|
if pattern.search(text):
|
|
if confidence > best_confidence:
|
|
best_intent = intent_name
|
|
best_confidence = confidence
|
|
|
|
# Extract entities
|
|
entities = {}
|
|
for entity_name, pattern in _ENTITY_PATTERNS.items():
|
|
match = pattern.search(text)
|
|
if match:
|
|
# Pick the first non-None capture group (handles alternation)
|
|
value = next((g for g in match.groups() if g is not None), None)
|
|
if value:
|
|
entities[entity_name] = value
|
|
|
|
intent = Intent(
|
|
name=best_intent,
|
|
confidence=best_confidence,
|
|
entities=entities,
|
|
raw_text=text,
|
|
)
|
|
logger.debug("NLU: '%s' → %s (%.2f)", text[:50], intent.name, intent.confidence)
|
|
return intent
|
|
|
|
|
|
def extract_command(text: str) -> str | None:
|
|
"""Extract a direct command from text, if present.
|
|
|
|
Commands are prefixed with '/' or 'timmy,' — e.g.:
|
|
/status
|
|
timmy, spawn agent Echo
|
|
"""
|
|
text = text.strip()
|
|
|
|
# Slash commands
|
|
if text.startswith("/"):
|
|
return text[1:].strip().split()[0] if len(text) > 1 else None
|
|
|
|
# "timmy," prefix
|
|
match = re.match(r"timmy[,:]?\s+(.+)", text, re.IGNORECASE)
|
|
if match:
|
|
return match.group(1).strip()
|
|
|
|
return None
|