Multi-Intent Detection: Handling Users Who Ask Multiple Things in One Message
Learn how to detect and handle multiple intents in a single user message, including intent splitting, parallel processing, and delivering coherent ordered responses.
The Single-Intent Assumption Problem
Most conversational AI systems assume each user message contains exactly one intent. But users naturally combine requests: "Check my balance and transfer $200 to savings." That single message carries two distinct intents — a balance inquiry and a fund transfer. Agents that only detect one intent frustrate users by ignoring part of their request.
Multi-intent detection identifies all intents within a message, separates them, processes each one, and delivers a coherent combined response.
Intent Segmentation
The first step is splitting a compound message into individual intent segments. Coordinating conjunctions ("and," "also," "then") and punctuation are natural delimiters.
import re
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class IntentSegment:
text: str
intent: Optional[str] = None
confidence: float = 0.0
entities: dict = field(default_factory=dict)
response: Optional[str] = None
order: int = 0
class IntentSplitter:
def __init__(self):
self.split_patterns = [
r"and(?:s+also)?",
r"then",
r"also",
r"plus",
r"[;.](?=s)",
r"after that",
]
self.combined_pattern = "|".join(
f"({p})" for p in self.split_patterns
)
def split(self, message: str) -> list[IntentSegment]:
segments = re.split(
self.combined_pattern, message, flags=re.IGNORECASE
)
# Filter out None values and delimiter matches
cleaned = [
s.strip() for s in segments
if s and s.strip() and not re.match(
self.combined_pattern, s.strip(), re.IGNORECASE
)
]
if not cleaned:
return [IntentSegment(text=message, order=0)]
return [
IntentSegment(text=seg, order=i)
for i, seg in enumerate(cleaned)
if len(seg) > 2 # Skip very short fragments
]
Intent Classification Pipeline
After splitting, classify each segment independently. This example uses a keyword-based classifier, but in production you would use a trained model or LLM.
class IntentClassifier:
def __init__(self):
self.intent_patterns = {
"check_balance": {
"keywords": ["balance", "how much", "account"],
"base_confidence": 0.8,
},
"transfer": {
"keywords": ["transfer", "send", "move"],
"base_confidence": 0.8,
},
"pay_bill": {
"keywords": ["pay", "bill", "payment"],
"base_confidence": 0.75,
},
"order_status": {
"keywords": ["order", "tracking", "shipment", "delivery"],
"base_confidence": 0.8,
},
}
def classify(self, segment: IntentSegment) -> IntentSegment:
text_lower = segment.text.lower()
best_intent = None
best_score = 0.0
for intent, config in self.intent_patterns.items():
matches = sum(
1 for kw in config["keywords"] if kw in text_lower
)
if matches > 0:
score = config["base_confidence"] * (
matches / len(config["keywords"])
)
if score > best_score:
best_score = score
best_intent = intent
segment.intent = best_intent or "unknown"
segment.confidence = best_score
return segment
Parallel Processing and Ordered Response
Process intents in parallel when they are independent, but maintain the user's original ordering in the response.
See AI Voice Agents Handle Real Calls
Book a free demo or calculate how much you can save with AI voice automation.
import asyncio
from typing import Callable
class MultiIntentProcessor:
def __init__(self):
self.splitter = IntentSplitter()
self.classifier = IntentClassifier()
self.handlers: dict[str, Callable] = {}
def register_handler(self, intent: str, handler: Callable):
self.handlers[intent] = handler
async def process(self, user_message: str) -> str:
segments = self.splitter.split(user_message)
# Classify all segments
classified = [self.classifier.classify(seg) for seg in segments]
# Process independent intents concurrently
tasks = []
for seg in classified:
handler = self.handlers.get(seg.intent)
if handler:
tasks.append(self._execute(seg, handler))
else:
seg.response = f"I'm not sure how to help with: {seg.text}"
tasks.append(asyncio.sleep(0)) # no-op placeholder
await asyncio.gather(*tasks)
# Combine responses in original order
responses = sorted(classified, key=lambda s: s.order)
parts = [s.response for s in responses if s.response]
return "\n\n".join(parts)
async def _execute(self, segment: IntentSegment, handler: Callable):
try:
segment.response = await handler(segment)
except Exception as e:
segment.response = (
f"I encountered an issue processing '{segment.text}': {e}"
)
Wiring Up Handlers
async def handle_balance(segment: IntentSegment) -> str:
# Simulated balance check
return "Your current balance is $2,450.00."
async def handle_transfer(segment: IntentSegment) -> str:
return "Transfer of $200 to savings has been initiated."
processor = MultiIntentProcessor()
processor.register_handler("check_balance", handle_balance)
processor.register_handler("transfer", handle_transfer)
# Usage
result = asyncio.run(
processor.process("Check my balance and transfer $200 to savings")
)
print(result)
# Your current balance is $2,450.00.
#
# Transfer of $200 to savings has been initiated.
Handling Intent Dependencies
Some compound requests have implicit dependencies. "Check my balance and transfer everything to savings" requires the balance result before the transfer can execute. Detect these dependencies and process them sequentially.
class DependencyResolver:
def __init__(self):
self.dependency_rules = {
("check_balance", "transfer"): self._check_transfer_dep,
}
def _check_transfer_dep(self, segments: list[IntentSegment]) -> bool:
transfer_seg = next(
(s for s in segments if s.intent == "transfer"), None
)
if transfer_seg and "everything" in transfer_seg.text.lower():
return True # Transfer depends on balance result
return False
def has_dependency(self, segments: list[IntentSegment]) -> bool:
intents = tuple(s.intent for s in segments)
for rule_key, checker in self.dependency_rules.items():
if all(i in intents for i in rule_key):
if checker(segments):
return True
return False
FAQ
How do you avoid splitting single intents that use coordinating conjunctions?
Not every "and" separates intents. "Search for flights to Paris and London" is a single search intent with two destinations. Use syntactic analysis to distinguish coordinated arguments from coordinated clauses. Train your splitter on labeled examples from your domain, and when in doubt, keep the message whole and let the classifier handle multi-entity extraction within one intent.
What if the intents conflict with each other?
Conflicting intents like "cancel my order and add expedited shipping" should be flagged before processing. Build a conflict matrix of intent pairs that are mutually exclusive. When detected, ask the user to clarify which action they prefer rather than executing one and silently dropping the other.
How do you handle more than three intents in one message?
Messages with four or more intents are rare but happen. Process them all, but present the responses with clear visual separation — numbered items or headers for each. If processing all would exceed a time budget, acknowledge the full list and process them in batches, confirming each before continuing.
#MultiIntent #NLU #IntentDetection #ConversationalAI #Python #AgenticAI #LearnAI #AIEngineering
CallSphere Team
Expert insights on AI voice agents and customer communication automation.
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.