Intent Router Architecture¶
Location: app/orchestrator/intent_router.py
Created: 2025-12-02
Overview¶
The Intent Router is the brain that decides where to route user messages when there's an active MiniApp session. It replaces the old "relevance classifier" with a more principled approach based on user intent.
The Core Problem It Solves¶
Old approach asked: "Is this message relevant to the MiniApp topic?" - ❌ "What are the best hotels?" → Relevant to trip → Routed to MiniApp → Added as venue
New approach asks: "Does the user want to perform an ACTION or ask a QUESTION?" - ✅ "What are the best hotels?" → CONVERSATIONAL intent → Routed to GPT-5
Intent Types¶
| Intent | Description | Example | Routing |
|---|---|---|---|
CONVERSATIONAL |
Questions, opinions, seeking information | "What are the best hotels?" | → GPT-5 |
TRANSACTIONAL |
Add, remove, modify, record | "Add Park Hyatt to my list" | → MiniApp |
NAVIGATIONAL |
View, list, check status | "Show my venues" | → MiniApp |
META |
Session control | "Cancel", "Done" | → MiniApp |
Architecture¶
┌─────────────────────────────────────────────────────────┐
│ Message Received │
└─────────────────────────┬───────────────────────────────┘
│
▼
┌───────────────────────┐
│ New Trigger Detected? │───YES──► Start MiniApp Session
└───────────┬───────────┘
│ NO
▼
┌───────────────────────┐
│ Active MiniApp │───NO───► Route to GPT-5
│ Session? │
└───────────┬───────────┘
│ YES
▼
┌───────────────────────┐
│ Fast Path Check │
│ (0ms, no LLM call) │
└───────────┬───────────┘
│
┌────────────────┼────────────────┐
▼ ▼ ▼
[Obvious [Obvious [Ambiguous]
CONVERSATIONAL] ACTION] │
│ │ ▼
│ │ ┌───────────────────────┐
│ │ │ LLM Classification │
│ │ │ (gpt-5-nano) │
│ │ │ ~100ms │
│ │ └───────────┬───────────┘
│ │ │
▼ ▼ ▼
GPT-5 MiniApp Intent Decision
Fast Path Optimizations¶
The Intent Router includes several fast paths that skip the LLM call entirely:
1. Direct Assistant Address Without Action Keywords¶
# "Sage what are the best hotels?" → Fast CONVERSATIONAL
# "Sage add Bar Mood" → Falls through to LLM (has "add")
assistant_patterns = ["sage ", "echo ", "hey sage", "@sage"]
action_keywords = ["add", "remove", "show", "list", "visited", ...]
if starts_with_assistant and not has_action_keyword:
return CONVERSATIONAL # Fast path, no LLM
2. Obvious Conversational Patterns¶
conversational_patterns = [
"what's the weather", "how are you", "tell me about",
"what do you think", "can you explain", "do you recommend",
"what are the best", "where should i", ...
]
if any(pattern in message):
return CONVERSATIONAL # Fast path, no LLM
3. Session Control Keywords¶
meta_patterns = ["cancel", "stop", "quit", "done", "nevermind"]
if any(pattern in message) and active_session:
return META → MiniApp # Fast path, no LLM
4. No Active Session¶
LLM Classification¶
For ambiguous messages that don't match fast paths, we use gpt-5-nano:
System Prompt (Key Excerpt)¶
The key question is NOT "is this message about {app_name}?"
The key question IS "does the user want to PERFORM AN ACTION or ASK A QUESTION?"
### CONVERSATIONAL Intent (→ General Assistant)
User wants information, opinions, advice, or general chat.
Even if the topic relates to {app_name}, informational questions go to the general assistant.
Examples of CONVERSATIONAL (even during active trip planning):
- "What are the best hotels in Shanghai?" (seeking information)
- "Is the Bund worth visiting?" (seeking opinion)
- "Tell me about Shanghai's food scene" (seeking knowledge)
### TRANSACTIONAL Intent (→ MiniApp)
User wants to ADD, REMOVE, MODIFY, or RECORD something.
Examples:
- "Add Park Hyatt to my list"
- "Remove that restaurant"
- "Mark the noodle place as visited"
Response Format¶
{
"intent": "CONVERSATIONAL" | "TRANSACTIONAL" | "NAVIGATIONAL" | "META",
"target": "general_assistant" | "miniapp",
"confidence": 0.0-1.0,
"reasoning": "brief explanation",
"action": "optional: add_venue, view_list, end_session"
}
Usage¶
In MiniAppRouter¶
from app.orchestrator.intent_router import get_intent_router, RoutingTarget
# Get singleton instance
intent_router = get_intent_router()
# Classify message
routing_decision = await intent_router.classify_and_route(
message=request.text,
active_miniapp_id=active_session.mini_app_id,
session_data=dict(active_session.state_data),
conversation_history=recent_messages,
)
# Route based on decision
if routing_decision.target == RoutingTarget.MINIAPP:
# Handle with MiniApp
...
else:
# Fall through to GPT-5
return False
Performance¶
| Path | Latency | When Used |
|---|---|---|
| Fast path (no LLM) | ~1ms | Obvious patterns, assistant address |
| LLM classification | ~100ms | Ambiguous messages |
| Error fallback | ~1ms | Classification errors → CONVERSATIONAL |
Configuration¶
The Intent Router uses the following LLM settings:
self.llm = LLMClient(model="gpt-5-nano")
# In classify_and_route:
response = self.llm.generate_response(
system_prompt=system_prompt,
user_message=user_prompt,
max_tokens=200, # Short responses only
)
Error Handling¶
On any error during classification, the router defaults to CONVERSATIONAL:
except Exception as e:
logger.error(f"[IntentRouter] LLM error: {e}")
return RoutingDecision(
intent=IntentType.CONVERSATIONAL,
target=RoutingTarget.GENERAL_ASSISTANT,
confidence=0.5,
reasoning=f"Classification error, defaulting to conversational",
)
Rationale: CONVERSATIONAL is the safer default because it doesn't modify state. A question routed to GPT-5 is harmless; an action accidentally routed to MiniApp could corrupt data.
Testing¶
Unit Test Examples¶
# Test conversational detection
async def test_question_routes_to_gpt5():
router = IntentRouter()
result = await router.classify_and_route(
message="What are the best hotels in Shanghai?",
active_miniapp_id="trip_planner",
session_data={"destination": "Shanghai"},
)
assert result.intent == IntentType.CONVERSATIONAL
assert result.target == RoutingTarget.GENERAL_ASSISTANT
# Test action detection
async def test_add_command_routes_to_miniapp():
router = IntentRouter()
result = await router.classify_and_route(
message="Add Park Hyatt to my list",
active_miniapp_id="trip_planner",
session_data={"destination": "Shanghai"},
)
assert result.intent == IntentType.TRANSACTIONAL
assert result.target == RoutingTarget.MINIAPP
Extending Intent Types¶
To add new intent types:
-
Add to
IntentTypeenum: -
Update system prompt with examples
-
Update routing logic in
classify_and_route
Related Files¶
app/orchestrator/intent_router.py- Main implementationapp/orchestrator/miniapp_router.py- Uses IntentRouterapp/miniapps/routing/relevance_classifier.py- Old classifier (deprecated)docs/decisions/005-intent-based-miniapp-routing.md- ADR