Skip to content

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

if not active_miniapp_id:
    return CONVERSATIONAL  # Let trigger detection handle new sessions

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:

  1. Add to IntentType enum:

    class IntentType(Enum):
        CONVERSATIONAL = "conversational"
        TRANSACTIONAL = "transactional"
        NAVIGATIONAL = "navigational"
        META = "meta"
        FEEDBACK = "feedback"  # NEW: User giving feedback
    

  2. Update system prompt with examples

  3. Update routing logic in classify_and_route


  • app/orchestrator/intent_router.py - Main implementation
  • app/orchestrator/miniapp_router.py - Uses IntentRouter
  • app/miniapps/routing/relevance_classifier.py - Old classifier (deprecated)
  • docs/decisions/005-intent-based-miniapp-routing.md - ADR