Spaces:
Running
Running
Pulastya B
Fixed an issue due to which the frontend never recieved that the event was completed and was stuck in an infinite loop
2e3162d | """ | |
| Intent Classifier - Determines execution mode for the Reasoning Loop. | |
| Three execution modes: | |
| 1. DIRECT: "Make a scatter plot" β SBERT routing β tool β done | |
| - Clear, specific command with obvious tool mapping | |
| - No reasoning loop needed | |
| 2. INVESTIGATIVE: "Why are customers churning?" β reasoning loop | |
| - Analytical question requiring hypothesis testing | |
| - Reasoning loop drives tool selection | |
| 3. EXPLORATORY: "Analyze this data" β auto-hypothesis β reasoning loop | |
| - Open-ended request with no specific question | |
| - First profiles data, generates hypotheses, then investigates | |
| Classification strategy (3-phase): | |
| Phase 1: Regex fast-path β catches obvious patterns instantly (0ms) | |
| Phase 2: SBERT semantic similarity β handles novel phrasings (~5ms) | |
| Phase 3: Keyword heuristic fallback β when SBERT unavailable | |
| """ | |
| import re | |
| import numpy as np | |
| from typing import Optional, Dict, Any, Tuple, List | |
| from dataclasses import dataclass | |
| class IntentResult: | |
| """Result of intent classification.""" | |
| mode: str # "direct", "investigative", "exploratory" | |
| confidence: float # 0.0-1.0 | |
| reasoning: str # Why this mode was chosen | |
| sub_intent: Optional[str] # More specific intent (e.g., "visualization", "cleaning") | |
| # Patterns that indicate DIRECT mode (specific tool commands) | |
| DIRECT_PATTERNS = [ | |
| # Visualization commands | |
| (r"\b(make|create|generate|build|show|draw|plot)\b.*(scatter|histogram|heatmap|box\s*plot|bar\s*chart|pie\s*chart|line\s*chart|dashboard|time\s*series)", "visualization"), | |
| (r"\b(scatter|histogram|heatmap|boxplot|bar\s*chart)\b.*\b(of|for|between|showing)\b", "visualization"), | |
| # Data cleaning commands | |
| (r"\b(clean|remove|drop|fill|impute|handle)\b.*(missing|null|nan|outlier|duplicate)", "cleaning"), | |
| (r"\b(fix|convert|change)\b.*(data\s*type|dtype|column\s*type)", "cleaning"), | |
| # Feature engineering commands | |
| (r"\b(create|add|extract|generate)\b.*(feature|time\s*feature|interaction|encoding)", "feature_engineering"), | |
| (r"\b(encode|one-hot|label\s*encode|ordinal)\b.*\b(categorical|column)", "feature_engineering"), | |
| # Model training commands | |
| (r"\b(train|build|fit|run)\b.*(model|classifier|regressor|baseline|xgboost|random\s*forest)", "training"), | |
| (r"\b(tune|optimize)\b.*\b(hyperparameter|model|parameter)", "training"), | |
| (r"\b(cross[\s-]?valid)", "training"), | |
| # Profiling commands | |
| (r"\b(profile|describe|summarize)\b.*\b(dataset|data|table|file)", "profiling"), | |
| (r"\b(data\s*quality|quality\s*check|check\s*quality)", "profiling"), | |
| # Report generation | |
| (r"\b(generate|create|build)\b.*\b(report|eda\s*report|profiling\s*report)", "reporting"), | |
| ] | |
| # Patterns that indicate INVESTIGATIVE mode (analytical questions) | |
| INVESTIGATIVE_PATTERNS = [ | |
| # Causal / explanatory questions | |
| (r"\bwhy\b.*(are|is|do|does|did)\b", "causal"), | |
| (r"\bwhat\b.*(cause|driv|factor|reason|explain|lead)", "causal"), | |
| (r"\bwhat\b.*(affect|impact|influence|determine)", "causal"), | |
| # Imperative analytical commands ("Explain X", "Identify Y", "Show me what drives Z") | |
| (r"\b(explain|describe|interpret|assess|evaluate|examine|investigate|understand)\b.*(feature|importance|correlation|distribution|relationship|data|model|pattern|variable|column|factor)", "analytical_imperative"), | |
| (r"\b(identify|find|determine|show|reveal)\b.*(important|key|significant|driving|top|main|critical|relevant)\b.*(feature|factor|variable|column|predictor|driver)", "feature_importance"), | |
| (r"\b(feature|variable)\b.*\b(importance|ranking|significance|selection|relevance)", "feature_importance"), | |
| (r"\b(important|key|significant|top|main|driving)\b.*\b(feature|factor|variable|column|predictor)", "feature_importance"), | |
| (r"\b(what|which)\b.*\b(feature|variable|column|factor)\b.*\b(important|matter|significant|relevant|impact)", "feature_importance"), | |
| # Relationship / correlation questions | |
| (r"\bhow\b.*(does|do|is|are)\b.*\b(relate|correlat|affect|impact|change|vary)", "relationship"), | |
| (r"\b(relationship|correlation|association)\b.*\bbetween\b", "relationship"), | |
| (r"\bcorrelat", "relationship"), | |
| # Comparison questions | |
| (r"\b(differ|compar|contrast)\b.*\bbetween\b", "comparison"), | |
| (r"\bwhich\b.*(better|worse|higher|lower|more|less|best|worst)", "comparison"), | |
| # Pattern / trend questions | |
| (r"\b(pattern|trend|anomal|outlier|unusual|interesting)\b", "pattern"), | |
| (r"\bis\s+there\b.*(pattern|trend|relationship|correlation|difference)", "pattern"), | |
| # Prediction-oriented questions (but NOT direct "train a model" commands) | |
| (r"\bcan\s+(we|i|you)\b.*(predict|forecast|estimate|determine)", "predictive"), | |
| (r"\bwhat\b.*(predict|forecast|expect|happen)", "predictive"), | |
| # Segmentation / grouping questions | |
| (r"\b(segment|group|cluster|categori)\b", "segmentation"), | |
| (r"\bwhat\b.*(type|kind|group|segment)\b.*\b(customer|user|product)", "segmentation"), | |
| ] | |
| # Patterns that indicate EXPLORATORY mode (open-ended requests) | |
| EXPLORATORY_PATTERNS = [ | |
| (r"^analyze\b.*\b(this|the|my)\b.*\b(data|dataset|file|csv)", "general_analysis"), | |
| (r"^(tell|show)\b.*\b(me|us)\b.*\b(about|everything|what)", "general_analysis"), | |
| (r"^(explore|investigate|examine|look\s*(at|into))\b.*\b(this|the|my)\b", "general_analysis"), | |
| (r"^what\b.*\b(can|do)\b.*\b(you|we)\b.*\b(find|learn|discover|see)", "general_analysis"), | |
| (r"^(give|provide)\b.*\b(overview|summary|insight|analysis)", "general_analysis"), | |
| (r"^(run|do|perform)\b.*\b(full|complete|comprehensive|end.to.end)\b.*\b(analysis|pipeline|workflow)", "full_pipeline"), | |
| (r"^(find|discover|uncover)\b.*\b(insight|pattern|trend|interesting)", "general_analysis"), | |
| ] | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # SBERT EXEMPLAR QUERIES β one embedding per mode, computed once on first call. | |
| # Add new examples here to improve semantic coverage without touching regex. | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SBERT_EXEMPLARS: Dict[str, List[str]] = { | |
| "direct": [ | |
| "Make a scatter plot of age vs income", | |
| "Create a histogram for the salary column", | |
| "Generate an EDA report", | |
| "Build a bar chart showing revenue by region", | |
| "Clean missing values in the dataset", | |
| "Handle outliers in the price column", | |
| "Remove duplicate rows", | |
| "Encode categorical columns", | |
| "Fix data types", | |
| "Train a random forest classifier", | |
| "Train a model to predict churn", | |
| "Tune hyperparameters for the best model", | |
| "Run cross validation on the model", | |
| "Generate a profiling report", | |
| "Create a heatmap of correlations", | |
| "Build a dashboard for this data", | |
| "Split data into train and test sets", | |
| "Scale numeric features", | |
| "Create time-based features from the date column", | |
| "Export predictions to CSV", | |
| ], | |
| "investigative": [ | |
| "Why are customers churning?", | |
| "What factors drive revenue?", | |
| "Explain feature importance in this dataset", | |
| "What is the relationship between price and demand?", | |
| "Which features are most important for predicting sales?", | |
| "How does age affect purchase behavior?", | |
| "What causes high employee attrition?", | |
| "Identify the key drivers of customer satisfaction", | |
| "Is there a correlation between marketing spend and conversions?", | |
| "Compare performance across different segments", | |
| "What patterns exist in the transaction data?", | |
| "Are there any anomalies or outliers worth investigating?", | |
| "Describe the distribution of income across groups", | |
| "Show me what impacts delivery time the most", | |
| "Break down the key factors behind loan defaults", | |
| "Determine which variables matter for this outcome", | |
| "Assess the statistical significance of these features", | |
| "Evaluate the relationship between temperature and energy usage", | |
| "Find what differentiates high-value and low-value customers", | |
| "Uncover hidden patterns in usage behavior", | |
| ], | |
| "exploratory": [ | |
| "Analyze this dataset", | |
| "What can you find in this data?", | |
| "Explore the data and tell me what's interesting", | |
| "Give me an overview of this dataset", | |
| "Run a full analysis on this file", | |
| "Look at this data and find insights", | |
| "Tell me everything about this dataset", | |
| "Do a comprehensive analysis", | |
| "What does this data look like?", | |
| "Examine this CSV and summarize findings", | |
| "Discover insights from this data", | |
| "Perform end to end analysis on this dataset", | |
| "What's in this data?", | |
| "Summarize the key trends and patterns", | |
| "Provide a complete data exploration", | |
| ], | |
| } | |
| class IntentClassifier: | |
| """ | |
| Classifies user intent into one of three execution modes. | |
| 3-phase classification strategy: | |
| 1. Regex fast-path β catches obvious patterns (0ms, ~70% of queries) | |
| 2. SBERT semantic similarity β handles novel phrasings (~5ms) | |
| 3. Keyword heuristic fallback β when SBERT unavailable | |
| When a SemanticLayer is provided (has a loaded SBERT model), exemplar | |
| queries for each mode are embedded once and cached. New queries are | |
| classified by cosine similarity to these exemplars β no regex needed. | |
| Usage: | |
| from src.utils.semantic_layer import get_semantic_layer | |
| classifier = IntentClassifier(semantic_layer=get_semantic_layer()) | |
| result = classifier.classify("Why are customers churning?") | |
| # IntentResult(mode="investigative", confidence=0.9, ...) | |
| # Also works without SBERT (regex + heuristic only): | |
| classifier = IntentClassifier() | |
| result = classifier.classify("Make a scatter plot of age vs income") | |
| """ | |
| def __init__(self, semantic_layer=None): | |
| """ | |
| Args: | |
| semantic_layer: Optional SemanticLayer instance with loaded SBERT model. | |
| If provided, enables semantic intent classification. | |
| """ | |
| self.semantic_layer = semantic_layer | |
| self._exemplar_embeddings = None # Lazy-computed: {mode: np.ndarray} | |
| def _ensure_exemplar_embeddings(self): | |
| """Lazily compute and cache SBERT embeddings for exemplar queries.""" | |
| if self._exemplar_embeddings is not None: | |
| return | |
| if not self.semantic_layer or not self.semantic_layer.enabled: | |
| return | |
| try: | |
| self._exemplar_embeddings = {} | |
| for mode, exemplars in SBERT_EXEMPLARS.items(): | |
| embeddings = self.semantic_layer.model.encode( | |
| exemplars, convert_to_numpy=True, | |
| show_progress_bar=False, batch_size=32 | |
| ) | |
| self._exemplar_embeddings[mode] = embeddings # shape: (N, dim) | |
| total = sum(len(v) for v in SBERT_EXEMPLARS.values()) | |
| print(f" π§ IntentClassifier: Cached {total} exemplar embeddings across 3 modes") | |
| except Exception as e: | |
| print(f" β οΈ IntentClassifier: Failed to encode exemplars: {e}") | |
| self._exemplar_embeddings = None | |
| def _classify_sbert(self, query: str) -> Optional[IntentResult]: | |
| """ | |
| Classify intent using SBERT semantic similarity to exemplar queries. | |
| For each mode, compute cosine similarity of the query to all exemplars | |
| in that mode, then take the max. The mode with the highest max-sim wins. | |
| Returns None if SBERT is unavailable or classification is ambiguous. | |
| """ | |
| self._ensure_exemplar_embeddings() | |
| if self._exemplar_embeddings is None: | |
| return None | |
| try: | |
| from sklearn.metrics.pairwise import cosine_similarity as cos_sim | |
| query_emb = self.semantic_layer.model.encode( | |
| query, convert_to_numpy=True, show_progress_bar=False | |
| ).reshape(1, -1) | |
| mode_scores = {} | |
| mode_best_exemplar = {} | |
| for mode, exemplar_embs in self._exemplar_embeddings.items(): | |
| sims = cos_sim(query_emb, exemplar_embs)[0] # shape: (N,) | |
| best_idx = int(np.argmax(sims)) | |
| mode_scores[mode] = float(sims[best_idx]) | |
| mode_best_exemplar[mode] = SBERT_EXEMPLARS[mode][best_idx] | |
| # Pick mode with highest score | |
| best_mode = max(mode_scores, key=mode_scores.get) | |
| best_score = mode_scores[best_mode] | |
| runner_up = sorted(mode_scores.values(), reverse=True)[1] | |
| margin = best_score - runner_up | |
| # Require minimum similarity AND reasonable margin | |
| if best_score < 0.35: | |
| # Too low similarity to any mode β fall through | |
| return None | |
| # Map raw cosine similarity (typically 0.4-0.9) to confidence (0.6-0.95) | |
| confidence = min(0.95, 0.55 + best_score * 0.45) | |
| # If margin is very thin, lower confidence | |
| if margin < 0.05: | |
| confidence = min(confidence, 0.60) | |
| best_match = mode_best_exemplar[best_mode] | |
| return IntentResult( | |
| mode=best_mode, | |
| confidence=round(confidence, 2), | |
| reasoning=f"SBERT semantic match (sim={best_score:.3f}, margin={margin:.3f}, closest: \"{best_match[:60]}\")", | |
| sub_intent="sbert_semantic" | |
| ) | |
| except Exception as e: | |
| print(f" β οΈ SBERT classification failed: {e}") | |
| return None | |
| def classify( | |
| self, | |
| query: str, | |
| dataset_info: Optional[Dict[str, Any]] = None, | |
| has_target_col: bool = False | |
| ) -> IntentResult: | |
| """ | |
| Classify user intent into execution mode. | |
| 3-phase strategy: | |
| Phase 1: Regex fast-path (catches ~70% of queries, 0ms) | |
| Phase 2: SBERT semantic similarity (handles novel phrasings, ~5ms) | |
| Phase 3: Keyword heuristic fallback (when SBERT unavailable) | |
| Args: | |
| query: User's natural language query | |
| dataset_info: Optional dataset schema info | |
| has_target_col: Whether user provided a target column | |
| Returns: | |
| IntentResult with mode, confidence, and reasoning | |
| """ | |
| query_lower = query.lower().strip() | |
| # ββ Phase 1: Regex fast-path (strongest evidence, instant) ββ | |
| direct_match = self._match_patterns(query_lower, DIRECT_PATTERNS) | |
| if direct_match: | |
| pattern, sub_intent = direct_match | |
| return IntentResult( | |
| mode="direct", | |
| confidence=0.90, | |
| reasoning=f"Direct command detected: {sub_intent} (pattern: {pattern[:50]})", | |
| sub_intent=sub_intent | |
| ) | |
| invest_match = self._match_patterns(query_lower, INVESTIGATIVE_PATTERNS) | |
| if invest_match: | |
| pattern, sub_intent = invest_match | |
| return IntentResult( | |
| mode="investigative", | |
| confidence=0.85, | |
| reasoning=f"Analytical question detected: {sub_intent}", | |
| sub_intent=sub_intent | |
| ) | |
| explore_match = self._match_patterns(query_lower, EXPLORATORY_PATTERNS) | |
| if explore_match: | |
| pattern, sub_intent = explore_match | |
| if sub_intent == "full_pipeline" and has_target_col: | |
| return IntentResult( | |
| mode="direct", | |
| confidence=0.85, | |
| reasoning="Full ML pipeline requested with target column", | |
| sub_intent="full_ml_pipeline" | |
| ) | |
| return IntentResult( | |
| mode="exploratory", | |
| confidence=0.80, | |
| reasoning=f"Open-ended analysis request: {sub_intent}", | |
| sub_intent=sub_intent | |
| ) | |
| # ββ Phase 2: SBERT semantic classification (handles novel queries) ββ | |
| sbert_result = self._classify_sbert(query) | |
| if sbert_result: | |
| # Apply special-case overrides | |
| if sbert_result.mode == "direct" and has_target_col: | |
| # If SBERT says direct but there's a target col + ML verbs, boost confidence | |
| if any(w in query_lower for w in ["predict", "train", "model", "classify"]): | |
| sbert_result.confidence = max(sbert_result.confidence, 0.80) | |
| return sbert_result | |
| # ββ Phase 3: Keyword heuristic fallback (no SBERT available) ββ | |
| return self._heuristic_classify(query_lower, has_target_col) | |
| def _match_patterns(self, query: str, patterns: list) -> Optional[Tuple[str, str]]: | |
| """Try to match query against a list of (pattern, sub_intent) tuples.""" | |
| for pattern, sub_intent in patterns: | |
| if re.search(pattern, query, re.IGNORECASE): | |
| return (pattern, sub_intent) | |
| return None | |
| def _heuristic_classify(self, query: str, has_target_col: bool) -> IntentResult: | |
| """Fallback classification using simple heuristics.""" | |
| # Question words β investigative | |
| if query.startswith(("why", "how", "what", "which", "is there", "are there", "does", "do")): | |
| return IntentResult( | |
| mode="investigative", | |
| confidence=0.60, | |
| reasoning="Query starts with question word, likely analytical", | |
| sub_intent="general_question" | |
| ) | |
| # Analytical imperative verbs β investigative | |
| if query.startswith(("explain", "describe", "interpret", "identify", | |
| "assess", "evaluate", "examine", "investigate", | |
| "determine", "understand", "show me", "tell me", | |
| "find the", "reveal", "uncover")): | |
| return IntentResult( | |
| mode="investigative", | |
| confidence=0.70, | |
| reasoning="Analytical imperative verb detected, likely investigative", | |
| sub_intent="analytical_imperative" | |
| ) | |
| # Very short queries β likely direct commands | |
| word_count = len(query.split()) | |
| if word_count <= 5: | |
| return IntentResult( | |
| mode="direct", | |
| confidence=0.55, | |
| reasoning="Short query, likely a direct command", | |
| sub_intent="short_command" | |
| ) | |
| # Has target column + action verbs β direct ML pipeline | |
| if has_target_col and any(w in query for w in ["predict", "train", "model", "classify", "regression"]): | |
| return IntentResult( | |
| mode="direct", | |
| confidence=0.75, | |
| reasoning="Target column provided with ML action verb", | |
| sub_intent="ml_pipeline" | |
| ) | |
| # Default: exploratory (safest default for data science) | |
| return IntentResult( | |
| mode="exploratory", | |
| confidence=0.40, | |
| reasoning="No strong pattern match, defaulting to exploratory analysis", | |
| sub_intent="default" | |
| ) | |
| def is_follow_up(query: str) -> bool: | |
| """ | |
| Detect if this is a follow-up question (uses context from previous analysis). | |
| Follow-ups should generally be INVESTIGATIVE (they're asking about | |
| something specific in the context of previous results). | |
| """ | |
| follow_up_patterns = [ | |
| r"^(now|next|also|and|then)\b", | |
| r"\b(the same|that|this|those|these)\b.*\b(data|model|result|plot|chart)", | |
| r"\b(more|another|different)\b.*\b(plot|chart|analysis|model)", | |
| r"\b(what about|how about|can you also)\b", | |
| r"\b(using|with)\b.*\b(the same|that|this)\b", | |
| ] | |
| query_lower = query.lower().strip() | |
| return any(re.search(p, query_lower) for p in follow_up_patterns) | |