Skip to main content

Norns Navigation Domain Supervisor

Status: Production
Location: /Users/ravenhelm/ravenhelm/docs/AI-ML-Platform/norns-agent/agent/agents/domain_supervisors/navigation_supervisor.py
Domain: Navigation
Level: Domain Supervisor (Level 2)

Overview

The Navigation Domain Supervisor coordinates navigation operations including directions, travel times, and place searches. It implements a sophisticated two-phase execution model and graceful error handling to ensure users never see raw API errors.

Architecture

Component Hierarchy

NornsSupervisor (Level 1)
└── NavigationDomainSupervisor (Level 2)
├── DirectionsWorker (Level 3)
└── PlacesWorker (Level 3)

Workers

WorkerPurposeOperations
DirectionsWorkerGet step-by-step directions and travel timesdirections, travel_time
PlacesWorkerSearch for nearby places (restaurants, stores, services)search

Core Features

1. Two-Phase Execution

The supervisor supports intelligent two-phase execution for "find X and get directions" queries:

Phase 1: Places Search

  • User: "Find tacos near me and get directions"
  • System searches for taco restaurants
  • Returns list of places with ratings and addresses

Phase 2: Automatic Directions

  • Extracts the top result from places search
  • Automatically gets directions to that location
  • Uses the actual place name and address (not a generic query)

Example Flow:

User: "Find me the best breakfast spot and send me directions"

Phase 1 (PlacesWorker):
→ Search for "best breakfast" near user's location
→ Return: "**The Buttermilk Cafe** 4.8/5\n 123 Main St, New Braunfels, TX"

Phase 2 (DirectionsWorker - automatic):
→ Get directions from current location to "The Buttermilk Cafe, 123 Main St, New Braunfels, TX"
→ Return: Step-by-step driving directions

2. Location Resolution System

The supervisor uses a three-tier fallback system for resolving "current location":

Priority Order:
1. Database (users.location field) - Most authoritative
2. Database (users.home_address field) - Fallback
3. Memory context - Legacy fallback
4. Default location (New Braunfels, TX) - Last resort

Database-First Approach:

async def _resolve_user_location(self, state: HierarchicalState) -> str:
# 1. Try current location field (e.g., "New Braunfels, TX")
if row["location"]:
return row["location"]

# 2. Try home address
if row["home_address"]:
return row["home_address"]

# 3. Extract city from location field
# 4. Use default

Why Database First?

  • More reliable than context extraction
  • User explicitly sets their location
  • Supports "near me" queries without parsing conversation
  • Works even with no prior conversation history

3. Graceful Error Handling

Never Expose Raw Errors - All API errors are converted to user-friendly messages:

ERROR_MESSAGES = {
"origin": "I need to know where you're starting from. Could you tell me your starting location?",
"destination": "I need to know where you want to go. Could you tell me your destination?",
"not defined": "I'm missing some location information. Could you provide both your starting point and destination?",
"api": "I'm having trouble connecting to the maps service right now. Please try again in a moment.",
"timeout": "The maps service is taking too long to respond. Please try again.",
"default": "I couldn't complete that navigation request. Could you try rephrasing it with specific locations?",
}

Example Error Flow:

API Error: "Error: origin parameter not defined"
User Sees: "I need to know where you're starting from. Could you tell me your starting location?"

4. Slack Delivery Integration

The Navigation supervisor handles its own Slack delivery - critical for preventing duplicate messages.

Key Rule: Only sends directions to Slack, not places lists.

# Detects Slack intent
if self._should_send_to_slack(action):
# Send only directions (more useful than places list)
directions_result = next(
(r for r in results if r.worker_name == "directions_worker" and r.success),
None
)
slack_message = str(directions_result.result) if directions_result else response

await self._send_to_slack(state, slack_message)

Slack Intent Detection:

def _should_send_to_slack(self, action: str) -> bool:
"""Detects phrases like send me, DM me, slack me, etc."""
return any(phrase in action_lower for phrase in [
"send me", "dm me", "slack me", "message me",
"send on slack", "send in slack", "send it to me",
"send to slack", "to my slack", "via slack",
"in slack", "slack"
])

Slack Error Messages (user-friendly):

SLACK_ERRORS = {
"missing_scope": "I need additional permissions to send you Slack DMs. The Slack app needs to be updated.",
"user_not_found": "I couldn't find your Slack account. Make sure your Slack is connected.",
"no_slack_id": "I don't have your Slack user ID on file yet.",
"connection": "I'm having trouble connecting to Slack right now.",
}

Routing Logic

The supervisor uses LLM-based routing with memory context awareness:

Routing Prompt

Available workers:
- directions: Get step-by-step directions or travel times between TWO KNOWN locations
- places: Search for nearby places (restaurants, stores, services)

IMPORTANT RULES:
1. If user wants to FIND a place (search), use "places" worker ONLY
2. If user wants directions to a SPECIFIC KNOWN place, use "directions" worker
3. If user wants to find a place AND get directions, use "places" worker ONLY
- directions will be handled automatically after we find the place
4. NEVER use placeholder values like "best taco place" as a destination

Routing Examples

User QueryWorker(s)Phase 2?Reasoning
"Find tacos near me"placesNoSearch only
"Directions to Costco"directionsNoKnown destination
"Find best breakfast and get directions"placesYesTwo-phase: search → directions
"How long to drive to Austin"directionsNoTravel time query

Critical Fix: Preventing Social Domain Parallel Execution

The Problem

Navigation supervisor handles its own Slack delivery, but the main supervisor was also routing to Social domain in parallel, causing:

  • Duplicate Slack messages
  • Confusing user experience
  • Wasted API calls

The Solution

Updated the main supervisor classification prompt to exclude Social domain when Navigation handles Slack:

File: /Users/ravenhelm/ravenhelm/docs/AI-ML-Platform/norns-agent/agent/agents/supervisor.py

Domain guide:
- navigation: directions, travel time, find restaurants, nearby places
(HANDLES ITS OWN SLACK DELIVERY - do NOT add social as parallel domain)
- social: slack messages, DM me, remind me, call me, notifications

Before Fix:

{
"domain": "navigation",
"parallel_domains": ["social"], // ❌ Causes duplicate Slack messages
"action": "Find tacos and send to Slack"
}

After Fix:

{
"domain": "navigation",
"parallel_domains": [], // ✅ Navigation handles Slack itself
"action": "Find tacos and send to Slack"
}

Implementation Details

  1. Main Supervisor (supervisor.py):

    • Classification prompt explicitly warns against Social + Navigation parallel routing
    • Navigation is flagged as self-contained for Slack delivery
  2. Navigation Supervisor (navigation_supervisor.py):

    • Detects Slack intent in user query
    • Sends only directions to Slack (not places list)
    • Provides user-friendly confirmation or error messages

Database Integration

User Location Lookup

# Query user's current location
SELECT location, home_address FROM users WHERE user_id = $1

# Fields:
# - location: Current/preferred location (e.g., "New Braunfels, TX")
# - home_address: Full home address (fallback)

Slack User ID Lookup

# Query user's Slack ID for DM delivery
SELECT slack_user_id FROM users WHERE user_id = $1

API Integration

Bifrost API (Slack Delivery)

Endpoint: POST /api/v1/mcp/slack/dm

async with httpx.AsyncClient(timeout=10.0) as client:
resp = await client.post(
f"{BIFROST_URL}/api/v1/mcp/slack/dm",
params={
"user_id": slack_user_id,
"message": message
},
)

Response Handling:

{
"success": true/false,
"error": "missing_scope|user_not_found|connection|..."
}

Streaming Architecture

The Navigation supervisor implements full streaming support:

Event Flow

1. tool_start (supervisor)
2. routing (worker selection)
3. tool_start (worker)
4. thinking (worker processing)
5. tool_end (worker)
6. worker_result
7. [Optional: Phase 2 - repeat 3-6 for directions]
8. thinking (Slack delivery if requested)
9. tool_end (supervisor)
10. domain_result (final response)

Stream Event Types

StreamEvent(type="tool_start", name="navigation_supervisor", ...)
StreamEvent(type="routing", content="Routing to workers: places", ...)
StreamEvent(type="thinking", phase="delivery", content="Sending directions to your Slack...", ...)
StreamEvent(type="domain_result", content="[Final response]", ...)

Usage Examples

User: "Find coffee shops near me"

Flow:

  1. Route to PlacesWorker
  2. Resolve location from database: "New Braunfels, TX"
  3. Search for coffee shops
  4. Return list with ratings and addresses

Response:

Places matching 'coffee shops':

• **Starbucks** 4.2/5
123 Main St, New Braunfels, TX 78130
Open now

• **Local Coffee Co** 4.7/5
456 Oak Ave, New Braunfels, TX 78130
Open now

Example 2: Two-Phase (Search + Directions)

User: "Find the best taco place and send me directions on Slack"

Flow:

  1. Phase 1: PlacesWorker searches for "best taco place"
    • Returns top results
  2. Phase 2: DirectionsWorker gets directions to #1 result
    • Uses actual place name and address
  3. Slack Delivery: Send only directions to Slack

Response:

Places matching 'best taco place':

• **Tacos El Rey** 4.9/5
789 River Rd, New Braunfels, TX 78130

[Directions from New Braunfels, TX to Tacos El Rey, 789 River Rd, New Braunfels, TX 78130]
Estimated time: 8 minutes (3.2 miles)

1. Head south on Main St
2. Turn right on River Rd
3. Destination on right

✓ Directions sent to your Slack\!

Example 3: Directions to Known Place

User: "How do I get to Costco from home?"

Flow:

  1. Route to DirectionsWorker
  2. Resolve origin: "home" → home_address from database
  3. Get directions to "Costco"

Response:

Directions from 123 Home St, New Braunfels, TX to Costco:
Estimated time: 15 minutes (6.5 miles)

1. Head north on Home St
2. Turn right on I-35
3. Take exit 189
...

Error Handling Examples

Missing Destination

API Error: "Error: destination parameter not defined"

User Sees: "I need to know where you want to go. Could you tell me your destination?"

Slack User Not Found

API Error: {"success": false, "error": "user_not_found"}

User Sees: "I couldn't find your Slack account. Make sure your Slack is connected."

Testing

Manual Test Cases

# Test 1: Simple search
curl -X POST https://norns.ravenhelm.dev/api/v1/chat/send \
-H "Authorization: Bearer $TOKEN" \
-d '{"message": "Find pizza near me"}'

# Test 2: Two-phase with Slack
curl -X POST https://norns.ravenhelm.dev/api/v1/chat/send \
-H "Authorization: Bearer $TOKEN" \
-d '{"message": "Find the best sushi place and slack me directions"}'

# Test 3: Direct directions
curl -X POST https://norns.ravenhelm.dev/api/v1/chat/send \
-H "Authorization: Bearer $TOKEN" \
-d '{"message": "Directions to Whole Foods"}'

Verification Checklist

  • Location resolved from database (not memory context)
  • Two-phase execution for "find + directions" queries
  • Only directions sent to Slack (not places list)
  • No duplicate Slack messages (Social domain not called)
  • User-friendly error messages (no raw API errors)
  • Slack user ID lookup works
  • Streaming events emitted properly

Troubleshooting

Issue: Duplicate Slack Messages

Symptom: User receives two identical Slack DMs

Cause: Social domain called in parallel with Navigation

Fix: Verify main supervisor prompt excludes Social when Navigation handles Slack

Check:

# In supervisor.py classification_prompt
"navigation: ... (HANDLES ITS OWN SLACK DELIVERY - do NOT add social as parallel domain)"

Issue: Wrong Location Used

Symptom: Directions start from wrong location

Cause: Location not in database, falling back to default

Fix: Update user's location in database

UPDATE users SET location = 'City, ST' WHERE user_id = 'uuid';

Issue: Places List Sent to Slack Instead of Directions

Symptom: User gets full search results on Slack instead of just directions

Cause: Slack delivery sending wrong result

Fix: Verify this code in _send_to_slack:

# Send only directions (not places list)
directions_result = next(
(r for r in results if r.worker_name == "directions_worker" and r.success),
None
)
slack_message = str(directions_result.result) if directions_result else response

Future Enhancements

  • Support multi-stop routes
  • Add transit/walking/biking options
  • Cache frequent destinations per user
  • Add voice output for directions
  • Support "remind me when I'm close" notifications
  • Integrate with calendar for travel time to events