| Emerging | Proven in #595 |
Users naturally combine multiple intents in single messages:
Traditional intent classification returns a single intent, causing:
This pattern addresses how to detect, decompose, and handle multiple intents in a single user message.
Core Concept: Separate intent detection from intent handling strategy.
The pattern enables strategy evolution without rewriting detection logic.
User Message
↓
┌─────────────────────┐
│ detect_multiple_ │ → MultiIntentResult
│ intents() │ - intents: List[Intent]
└─────────────────────┘ - is_multi_intent: bool
↓ - primary_intent
┌─────────────────────┐ - secondary_intents
│ Strategy Selection │ - has_greeting
│ (handle all/chain/ │ - has_substantive_intent
│ clarify) │
└─────────────────────┘
↓
Handler(s) Execute
@dataclass
class MultiIntentResult:
"""Result of multi-intent detection."""
intents: List[Intent] = field(default_factory=list)
original_message: str = ""
is_multi_intent: bool = False
@property
def primary_intent(self) -> Optional[Intent]:
"""Get primary intent - substantive over conversational."""
if not self.intents:
return None
# Substantive intents take precedence
for intent in self.intents:
if intent.category != IntentCategory.CONVERSATION:
return intent
return self.intents[0]
@property
def has_greeting(self) -> bool:
"""Check if greeting is among detected intents."""
return any(
i.category == IntentCategory.CONVERSATION and i.action == "greeting"
for i in self.intents
)
@property
def has_substantive_intent(self) -> bool:
"""Check for non-conversational intent."""
return any(i.category != IntentCategory.CONVERSATION for i in self.intents)
@staticmethod
def detect_multiple_intents(message: str) -> MultiIntentResult:
"""Detect ALL intents in a message."""
intents = []
# Check each pattern group (don't return early!)
pattern_groups = [
(GREETING_PATTERNS, IntentCategory.CONVERSATION, "greeting"),
(CALENDAR_PATTERNS, IntentCategory.QUERY, "meeting_time"),
(TODO_PATTERNS, IntentCategory.QUERY, "list_todos"),
# ... more pattern groups
]
for patterns, category, action in pattern_groups:
if matches_patterns(message, patterns):
intents.append(Intent(category=category, action=action))
return MultiIntentResult(
intents=intents,
original_message=message,
is_multi_intent=len(intents) > 1,
)
# In IntentService._process_intent_internal()
multi_result = await self.intent_classifier.classify_multiple(message)
if multi_result.is_multi_intent and multi_result.has_greeting and multi_result.has_substantive_intent:
# Use substantive intent for processing
intent = multi_result.primary_intent
# Mark greeting for response prefix
intent.context["multi_intent_greeting"] = True
# Later, when building response:
if intent.context.get("multi_intent_greeting"):
response = f"Hi there! {substantive_response}"
services/intent_service/pre_classifier.py - MultiIntentResult, detect_multiple_intents()services/intent_service/classifier.py - classify_multiple()services/intent/intent_service.py - Handle-all strategy in _process_intent_internal()tests/unit/services/test_multi_intent.py - 27 comprehensive testsPattern documented: January 21, 2026 Part of MUX-GATE-2 pattern discovery ceremony