intent_classifier.py•19.1 kB
"""Intent Classifier - Classifies user intent from natural language input"""
import re
from dataclasses import dataclass
from enum import Enum
from typing import Any, Dict, List, Optional, Tuple
class IntentType(Enum):
"""Types of user intents"""
CREATE = "create"
MODIFY = "modify"
DELETE = "delete"
QUERY = "query"
MEASURE = "measure"
EXPORT = "export"
IMPORT = "import"
VIEW = "view"
ANALYZE = "analyze"
HELP = "help"
CONFIGURE = "configure"
UNKNOWN = "unknown"
class ActionType(Enum):
"""Types of actions within intents"""
# Creation actions
PRIMITIVE = "primitive"
SKETCH = "sketch"
FEATURE = "feature"
ASSEMBLY = "assembly"
# Modification actions
TRANSFORM = "transform"
BOOLEAN = "boolean"
FILLET_CHAMFER = "fillet_chamfer"
PATTERN = "pattern"
# Query actions
PROPERTIES = "properties"
SELECTION = "selection"
DOCUMENT = "document"
# Analysis actions
GEOMETRY = "geometry"
PHYSICS = "physics"
INTERFERENCE = "interference"
@dataclass
class Intent:
"""Represents a classified intent"""
type: IntentType
action: Optional[ActionType]
confidence: float
entities: List[Dict[str, Any]]
parameters: Dict[str, Any]
original_text: str
keywords_matched: List[str]
class IntentClassifier:
"""
Classifies user intent from natural language input
to determine what action the user wants to perform.
"""
def __init__(self):
"""Initialize the Intent Classifier"""
self.intent_patterns = self._build_intent_patterns()
self.entity_patterns = self._build_entity_patterns()
self.parameter_extractors = self._build_parameter_extractors()
# Intent keywords mapping
self.intent_keywords = {
IntentType.CREATE: [
"create",
"make",
"build",
"add",
"new",
"generate",
"construct",
"draw",
"design",
"model",
"produce",
],
IntentType.MODIFY: [
"modify",
"change",
"edit",
"update",
"alter",
"adjust",
"transform",
"move",
"rotate",
"scale",
"resize",
"extend",
"trim",
],
IntentType.DELETE: [
"delete",
"remove",
"erase",
"clear",
"destroy",
"eliminate",
"cut",
"subtract",
],
IntentType.QUERY: [
"what",
"which",
"where",
"show",
"find",
"get",
"list",
"display",
"tell",
"info",
"information",
],
IntentType.MEASURE: [
"measure",
"calculate",
"compute",
"distance",
"length",
"area",
"volume",
"angle",
"dimension",
"size",
],
IntentType.EXPORT: [
"export",
"save",
"output",
"write",
"generate file",
"convert to",
],
IntentType.IMPORT: ["import", "load", "open", "read", "bring in"],
IntentType.VIEW: [
"view",
"look",
"zoom",
"pan",
"orbit",
"fit",
"focus",
"center",
],
IntentType.ANALYZE: [
"analyze",
"check",
"verify",
"validate",
"inspect",
"evaluate",
"test",
"examine",
],
IntentType.HELP: [
"help",
"how",
"explain",
"guide",
"tutorial",
"documentation",
"what is",
"how to",
],
IntentType.CONFIGURE: [
"configure",
"settings",
"preferences",
"setup",
"options",
"customize",
"config",
],
}
# Action keywords
self.action_keywords = {
ActionType.PRIMITIVE: [
"box",
"cube",
"cylinder",
"sphere",
"cone",
"torus",
],
ActionType.SKETCH: ["sketch", "2d", "profile", "drawing"],
ActionType.TRANSFORM: ["move", "translate", "rotate", "scale", "mirror"],
ActionType.BOOLEAN: [
"union",
"difference",
"intersection",
"combine",
"subtract",
],
ActionType.FILLET_CHAMFER: ["fillet", "round", "chamfer", "bevel"],
ActionType.PATTERN: ["pattern", "array", "duplicate", "copy", "repeat"],
}
def classify(self, text: str, context: Optional[Dict] = None) -> Intent:
"""
Classify intent from user input text
Args:
text: User input text
context: Optional context information
Returns:
Intent object with classification results
"""
text_lower = text.lower()
# Extract entities first
entities = self._extract_entities(text)
# Determine intent type
intent_type, intent_confidence, intent_keywords = self._classify_intent_type(
text_lower
)
# Determine action type
action_type = self._classify_action_type(text_lower, intent_type)
# Extract parameters
parameters = self._extract_parameters(text, intent_type, action_type)
# Enhance with context
if context:
self._enhance_with_context(intent_type, action_type, parameters, context)
# Adjust confidence based on clarity
confidence = self._calculate_confidence(
text_lower, intent_type, action_type, entities, parameters
)
return Intent(
type=intent_type,
action=action_type,
confidence=confidence,
entities=entities,
parameters=parameters,
original_text=text,
keywords_matched=intent_keywords,
)
def _build_intent_patterns(self) -> Dict[IntentType, List[re.Pattern]]:
"""Build regex patterns for intent detection"""
patterns = {}
# Create patterns
patterns[IntentType.CREATE] = [
re.compile(r"\b(create|make|build|add)\s+(a\s+)?(\w+)", re.I),
re.compile(r"\b(new|generate)\s+(\w+)", re.I),
re.compile(r"\b(draw|design)\s+(a\s+)?(\w+)", re.I),
]
patterns[IntentType.MODIFY] = [
re.compile(r"\b(modify|change|edit|update)\s+(\w+)", re.I),
re.compile(r"\b(move|rotate|scale)\s+(the\s+)?(\w+)?", re.I),
re.compile(r"\b(extend|trim|offset)\s+(\w+)", re.I),
]
patterns[IntentType.DELETE] = [
re.compile(r"\b(delete|remove|erase)\s+(the\s+)?(\w+)?", re.I),
re.compile(r"\b(clear|destroy)\s+(all\s+)?(\w+)?", re.I),
]
patterns[IntentType.QUERY] = [
re.compile(r"\b(what|which|where)\s+(is|are)\s+(\w+)", re.I),
re.compile(r"\b(show|display|list)\s+(me\s+)?(the\s+)?(\w+)?", re.I),
re.compile(r"\b(get|find)\s+(the\s+)?(\w+)", re.I),
]
patterns[IntentType.MEASURE] = [
re.compile(
r"\b(measure|calculate)\s+(the\s+)?(distance|length|area|volume)", re.I
),
re.compile(r"\b(what\s+is\s+the)\s+(distance|length|area|volume)", re.I),
re.compile(r"\b(compute|find)\s+(the\s+)?(dimension|size)", re.I),
]
return patterns
def _build_entity_patterns(self) -> Dict[str, re.Pattern]:
"""Build patterns for entity extraction"""
return {
"object_type": re.compile(
r"\b(box|cube|cylinder|sphere|cone|torus|sketch|part|assembly|body)\b",
re.I,
),
"dimension": re.compile(r"(\d+(?:\.\d+)?)\s*(mm|cm|m|inch|in|ft)?", re.I),
"color": re.compile(
r"\b(red|green|blue|yellow|orange|purple|black|white|gray|grey)\b", re.I
),
"position": re.compile(
r"at\s*\(?\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)?",
re.I,
),
"angle": re.compile(
r"(\d+(?:\.\d+)?)\s*(degrees?|deg|radians?|rad|°)", re.I
),
"count": re.compile(r"\b(\d+)\s+(times?|copies|instances?)\b", re.I),
"axis": re.compile(r"\b(x|y|z)[\s-]?axis\b", re.I),
"direction": re.compile(
r"\b(up|down|left|right|forward|backward|north|south|east|west)\b", re.I
),
"reference": re.compile(
r"\b(selected|current|active|last|previous|all)\b", re.I
),
}
def _build_parameter_extractors(self) -> Dict[str, callable]:
"""Build parameter extraction functions"""
def extract_dimensions(text):
"""Extract dimensional parameters"""
dims = []
pattern = re.compile(r"(\d+(?:\.\d+)?)\s*(mm|cm|m|inch|in)?", re.I)
for match in pattern.findall(text):
value = float(match[0])
unit = match[1] if match[1] else "mm"
dims.append({"value": value, "unit": unit})
return dims
def extract_position(text):
"""Extract position parameters"""
pattern = re.compile(
r"at\s*\(?\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)?",
re.I,
)
match = pattern.search(text)
if match:
return {
"x": float(match.group(1)),
"y": float(match.group(2)),
"z": float(match.group(3)),
}
return None
return {"dimensions": extract_dimensions, "position": extract_position}
def _classify_intent_type(self, text: str) -> Tuple[IntentType, float, List[str]]:
"""Classify the intent type from text"""
intent_scores = {}
matched_keywords = {}
# Check keywords
for intent_type, keywords in self.intent_keywords.items():
score = 0
matches = []
for keyword in keywords:
if keyword in text:
score += 1
matches.append(keyword)
if score > 0:
intent_scores[intent_type] = score
matched_keywords[intent_type] = matches
# Check patterns
for intent_type, patterns in self.intent_patterns.items():
for pattern in patterns:
if pattern.search(text):
intent_scores[intent_type] = intent_scores.get(intent_type, 0) + 2
# Determine best match
if intent_scores:
best_intent = max(intent_scores.items(), key=lambda x: x[1])
confidence = min(best_intent[1] / 5.0, 1.0) # Normalize confidence
keywords = matched_keywords.get(best_intent[0], [])
return best_intent[0], confidence, keywords
return IntentType.UNKNOWN, 0.0, []
def _classify_action_type(
self, text: str, intent_type: IntentType
) -> Optional[ActionType]:
"""Classify the action type based on intent and text"""
if intent_type == IntentType.CREATE:
# Check for primitive shapes
for keyword in self.action_keywords[ActionType.PRIMITIVE]:
if keyword in text:
return ActionType.PRIMITIVE
# Check for sketch
if "sketch" in text or "2d" in text:
return ActionType.SKETCH
elif intent_type == IntentType.MODIFY:
# Check for transformations
if any(word in text for word in ["move", "translate", "rotate", "scale"]):
return ActionType.TRANSFORM
# Check for boolean
if any(
word in text
for word in ["union", "difference", "intersection", "subtract"]
):
return ActionType.BOOLEAN
# Check for fillet/chamfer
if any(word in text for word in ["fillet", "round", "chamfer", "bevel"]):
return ActionType.FILLET_CHAMFER
return None
def _extract_entities(self, text: str) -> List[Dict[str, Any]]:
"""Extract entities from text"""
entities = []
for entity_type, pattern in self.entity_patterns.items():
matches = pattern.findall(text)
for match in matches:
entity = {
"type": entity_type,
"value": match,
"start": text.find(str(match)),
"end": text.find(str(match)) + len(str(match)),
}
entities.append(entity)
return entities
def _extract_parameters(
self, text: str, intent_type: IntentType, action_type: Optional[ActionType]
) -> Dict[str, Any]:
"""Extract parameters based on intent and action"""
parameters = {}
# Extract dimensions
if "dimensions" in self.parameter_extractors:
dims = self.parameter_extractors["dimensions"](text)
if dims:
if len(dims) >= 3:
parameters["length"] = dims[0]["value"]
parameters["width"] = dims[1]["value"]
parameters["height"] = dims[2]["value"]
elif len(dims) == 2:
parameters["length"] = dims[0]["value"]
parameters["width"] = dims[1]["value"]
elif len(dims) == 1:
parameters["size"] = dims[0]["value"]
# Extract position
if "position" in self.parameter_extractors:
pos = self.parameter_extractors["position"](text)
if pos:
parameters["position"] = pos
# Extract specific parameters based on intent
if intent_type == IntentType.CREATE and action_type == ActionType.PRIMITIVE:
# Check for specific shape parameters
if "radius" in text:
radius_match = re.search(
r"radius\s*(?:of\s*)?(\d+(?:\.\d+)?)", text, re.I
)
if radius_match:
parameters["radius"] = float(radius_match.group(1))
elif intent_type == IntentType.MODIFY and action_type == ActionType.TRANSFORM:
# Extract transformation parameters
angle_match = re.search(r"(\d+(?:\.\d+)?)\s*degrees?", text, re.I)
if angle_match:
parameters["angle"] = float(angle_match.group(1))
return parameters
def _enhance_with_context(
self,
intent_type: IntentType,
action_type: Optional[ActionType],
parameters: Dict[str, Any],
context: Dict,
):
"""Enhance classification with context information"""
# If there's a selection and intent is modify/delete, assume operation on selection
if context.get("selection", {}).get("has_selection"):
if intent_type in [
IntentType.MODIFY,
IntentType.DELETE,
IntentType.MEASURE,
]:
parameters["target"] = "selection"
# If no object specified but there's only one object, assume that's the target
if context.get("objects", {}).get("total_count") == 1:
if "target" not in parameters:
parameters["target"] = "current_object"
def _calculate_confidence(
self,
text: str,
intent_type: IntentType,
action_type: Optional[ActionType],
entities: List[Dict],
parameters: Dict,
) -> float:
"""Calculate overall confidence score"""
confidence = 0.5 # Base confidence
# Boost confidence for clear intent
if intent_type != IntentType.UNKNOWN:
confidence += 0.2
# Boost for specific action
if action_type is not None:
confidence += 0.1
# Boost for entities found
if entities:
confidence += min(len(entities) * 0.05, 0.15)
# Boost for parameters extracted
if parameters:
confidence += min(len(parameters) * 0.05, 0.15)
# Penalty for very short or very long text
text_length = len(text.split())
if text_length < 3 or text_length > 50:
confidence *= 0.8
return min(confidence, 1.0)
def explain_classification(self, intent: Intent) -> str:
"""Generate human-readable explanation of the classification"""
explanation = f"I understood that you want to {intent.type.value}"
if intent.action:
explanation += f" (specifically: {intent.action.value})"
if intent.entities:
objects = [
e["value"] for e in intent.entities if e["type"] == "object_type"
]
if objects:
explanation += f" involving {', '.join(objects)}"
if intent.parameters:
param_strs = []
for key, value in intent.parameters.items():
if key == "position":
param_strs.append(
f"at position ({value['x']}, {value['y']}, {value['z']})"
)
elif key in ["length", "width", "height", "radius"]:
param_strs.append(f"{key}: {value}")
if param_strs:
explanation += f" with {', '.join(param_strs)}"
explanation += f" (confidence: {intent.confidence:.0%})"
return explanation
def get_intent_suggestions(self, partial_text: str) -> List[str]:
"""Get intent suggestions for partial input"""
suggestions = []
for intent_type, keywords in self.intent_keywords.items():
for keyword in keywords:
if keyword.startswith(partial_text.lower()):
suggestions.append(keyword)
return list(set(suggestions))[:10]