| """ |
| RUBRA β intent_engine.py |
| Adaptive Intent Recognition System |
| |
| - Predicts what user ACTUALLY wants (not just literal words) |
| - Tracks intent history per session β learns patterns |
| - Self-updates routing decisions within chat |
| - Handles ambiguous requests gracefully |
| """ |
|
|
| import re |
| import json |
| import logging |
| from typing import Optional, List, Dict, Tuple |
| from dataclasses import dataclass, field |
|
|
| log = logging.getLogger("rubra.intent") |
|
|
| |
| |
| |
|
|
| INTENTS = { |
| |
| "greeting": {"agents": ["fast"], "priority": 10, "patterns": [r'\b(hi|hello|hey|salam|kemon|how are you|what\'s up)\b']}, |
| "farewell": {"agents": ["fast"], "priority": 10, "patterns": [r'\b(bye|goodbye|thanks|thank you|see you)\b']}, |
| "casual": {"agents": ["fast"], "priority": 8, "patterns": [r'\b(how|what do you think|opinion|feel|like)\b']}, |
|
|
| |
| "coding": {"agents": ["coding"], "priority": 9, "patterns": [ |
| r'\b(write|build|create|make|code|program|function|class|api|app|website|script)\b', |
| r'\b(debug|fix|error|bug|issue|broken|not working)\b', |
| r'\b(python|javascript|react|html|css|sql|bash|typescript)\b', |
| ]}, |
| "code_explain": {"agents": ["coding"], "priority": 8, "patterns": [ |
| r'\b(explain|understand|what does|how does|review|analyze)\b.*\b(code|function|class|script)\b', |
| ]}, |
| "run_code": {"agents": ["coding"], "priority": 9, "patterns": [ |
| r'\b(run|execute|test|output|result)\b.{0,20}\b(this|it|code|script)\b', |
| r'\b(run it|run this|execute this|test this)\b', |
| ]}, |
|
|
| |
| "search": {"agents": ["search"], "priority": 8, "patterns": [ |
| r'\b(weather|temperature|forecast)\b', |
| r'\b(price|bitcoin|crypto|stock|exchange rate)\b', |
| r'\b(news|latest|current|today|recent|trending)\b', |
| ]}, |
| "research": {"agents": ["browse"], "priority": 7, "patterns": [ |
| r'\b(research|find|look up|search for|browse|visit)\b', |
| r'https?://', |
| r'\b(who is|what is|tell me about)\b.{5,50}', |
| ]}, |
| "education": {"agents": ["tutor"], "priority": 7, "patterns": [ |
| r'\b(explain|teach|how does|what is|learn|study|understand)\b', |
| r'\b(math|physics|chemistry|biology|history|geography)\b', |
| r'\b(class|exam|homework|assignment|ssc|hsc|nctb)\b', |
| ]}, |
|
|
| |
| "creative": {"agents": ["general"], "priority": 6, "patterns": [ |
| r'\b(write|create|generate|compose)\b.{0,20}\b(story|poem|essay|article|email|letter)\b', |
| ]}, |
|
|
| |
| "file_analyze": {"agents": ["vision"], "priority": 9, "patterns": [ |
| r'\b(analyze|read|extract|summarize|what is in)\b.{0,20}\b(file|image|pdf|document|photo)\b', |
| ]}, |
|
|
| |
| "video": {"agents": ["coding"], "priority": 8, "patterns": [ |
| r'\b(video|render|animate|mp4|motion graphic)\b', |
| ]}, |
|
|
| |
| "project_gen": {"agents": ["coding"], "priority": 9, "patterns": [ |
| r'\b(generate|create|build|make)\b.{0,30}\b(project|app|website|system|platform|store)\b', |
| r'\b(full.?stack|nextjs|react app|fastapi|landing page|e.?commerce|online store)\b', |
| r'\b(separate.{0,15}files|multiple files|run the files)\b', |
| ]}, |
|
|
| |
| "general": {"agents": ["general"], "priority": 1, "patterns": []}, |
| } |
|
|
| |
| _COMPILED: Dict[str, List] = { |
| intent: [re.compile(p, re.IGNORECASE) for p in data["patterns"]] |
| for intent, data in INTENTS.items() |
| } |
|
|
| |
| |
| |
|
|
| def extract_context_signals(hist: List[Dict]) -> Dict: |
| """Extract signals from conversation history.""" |
| signals = { |
| "last_intent": None, |
| "has_code": False, |
| "has_file": False, |
| "topic": None, |
| "user_expertise": "intermediate", |
| "preferred_lang": "en", |
| "consecutive_coding": 0, |
| } |
| if not hist: |
| return signals |
|
|
| coding_count = 0 |
| for h in hist[-10:]: |
| content = h.get("content", "").lower() |
| role = h.get("role", "") |
|
|
| if role == "assistant": |
| if re.search(r'```(python|javascript|jsx|html|css|sql)', content): |
| signals["has_code"] = True |
| coding_count += 1 |
|
|
| if role == "user": |
| if re.search(r'[\u0980-\u09FF]', content): |
| signals["preferred_lang"] = "bn" |
| if re.search(r'\b(expert|senior|advanced|professional)\b', content): |
| signals["user_expertise"] = "expert" |
| elif re.search(r'\b(beginner|new|learning|student)\b', content): |
| signals["user_expertise"] = "beginner" |
|
|
| signals["consecutive_coding"] = coding_count |
| return signals |
|
|
|
|
| |
| |
| |
|
|
| @dataclass |
| class IntentResult: |
| intent: str |
| confidence: float |
| agent: str |
| reasoning: str = "" |
| sub_intent: Optional[str] = None |
|
|
| def classify_intent( |
| message: str, |
| hist: List[Dict] = None, |
| signals: Dict = None, |
| ) -> IntentResult: |
| """ |
| Classify user intent using: |
| 1. Pattern matching (fast) |
| 2. Context signals from history |
| 3. Implicit intent detection |
| """ |
| if hist is None: hist = [] |
| if signals is None: signals = extract_context_signals(hist) |
|
|
| lower = message.lower().strip() |
| scores: Dict[str, float] = {} |
|
|
| |
| for intent, patterns in _COMPILED.items(): |
| if not patterns: |
| scores[intent] = 0.1 |
| continue |
| match_count = sum(1 for p in patterns if p.search(lower)) |
| if match_count > 0: |
| base_score = INTENTS[intent]["priority"] * match_count |
| scores[intent] = base_score |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| if scores.get("project_gen", -1) >= scores.get("coding", -1) and "project_gen" in scores: |
| scores["project_gen"] += 0.01 |
|
|
| if signals.get("has_code") and re.search(r'\b(run|test|execute|it|this)\b', lower): |
| scores["run_code"] = scores.get("run_code", 0) + 15 |
|
|
| if signals.get("consecutive_coding", 0) >= 2: |
| scores["coding"] = scores.get("coding", 0) + 5 |
|
|
| |
| if re.search(r'\b(continue|more|next|keep going|go on|abar|aro)\b', lower): |
| last = signals.get("last_intent") |
| if last and last in scores: |
| scores[last] += 20 |
|
|
| |
| if not scores or max(scores.values()) <= 0: |
| return IntentResult( |
| intent="general", confidence=0.5, |
| agent="general", reasoning="No pattern match β defaulting to general" |
| ) |
|
|
| best_intent = max(scores, key=scores.get) |
| best_score = scores[best_intent] |
| total = sum(scores.values()) or 1 |
| confidence = min(best_score / total, 1.0) |
|
|
| agent = INTENTS[best_intent]["agents"][0] |
|
|
| return IntentResult( |
| intent = best_intent, |
| confidence = confidence, |
| agent = agent, |
| reasoning = f"Score {best_score:.1f} from {len([s for s in scores.values() if s > 0])} matching patterns", |
| ) |
|
|
|
|
| |
| |
| |
|
|
| class AdaptiveIntentTracker: |
| """ |
| Tracks intent patterns per session. |
| Adapts routing decisions based on user behavior. |
| """ |
|
|
| def __init__(self): |
| |
| self._sessions: Dict[str, Dict] = {} |
|
|
| def _get_session(self, sid: str) -> Dict: |
| if sid not in self._sessions: |
| self._sessions[sid] = { |
| "intents": [], |
| "corrections": [], |
| "preferences": {}, |
| "turn_count": 0, |
| } |
| return self._sessions[sid] |
|
|
| def track(self, sid: str, message: str, intent: IntentResult): |
| """Record intent for this turn.""" |
| s = self._get_session(sid) |
| s["intents"].append({ |
| "message": message[:80], |
| "intent": intent.intent, |
| "confidence": intent.confidence, |
| "agent": intent.agent, |
| }) |
| s["turn_count"] += 1 |
| |
| s["intents"] = s["intents"][-20:] |
|
|
| def get_session_context(self, sid: str) -> Dict: |
| """Get session-level context for better predictions.""" |
| s = self._get_session(sid) |
| intents = s["intents"] |
| if not intents: |
| return {} |
|
|
| |
| from collections import Counter |
| intent_counts = Counter(i["intent"] for i in intents) |
| dominant = intent_counts.most_common(1)[0][0] if intent_counts else "general" |
|
|
| |
| recent = [i["intent"] for i in intents[-3:]] |
|
|
| return { |
| "dominant_intent": dominant, |
| "recent_intents": recent, |
| "turn_count": s["turn_count"], |
| "last_intent": recent[-1] if recent else None, |
| } |
|
|
| def predict_next_intent(self, sid: str, message: str, hist: List[Dict]) -> IntentResult: |
| """ |
| Predict intent using both pattern matching and session history. |
| Self-corrects based on patterns learned in this session. |
| """ |
| session_ctx = self.get_session_context(sid) |
| signals = extract_context_signals(hist) |
|
|
| |
| if session_ctx.get("last_intent"): |
| signals["last_intent"] = session_ctx["last_intent"] |
|
|
| result = classify_intent(message, hist, signals) |
|
|
| |
| |
| if (session_ctx.get("dominant_intent") == "coding" |
| and result.intent == "general" |
| and re.search(r'\b(run|test|execute|it|this|try)\b', message, re.I)): |
| result = IntentResult( |
| intent = "run_code", |
| confidence = 0.85, |
| agent = "coding", |
| reasoning = "Session context: user in coding mode", |
| ) |
|
|
| self.track(sid, message, result) |
| return result |
|
|
|
|
| |
| intent_tracker = AdaptiveIntentTracker() |