Skip to content

Conversation State Manager

Overview

The ConversationStateManager is a unified system for managing conversation state in Archety. It combines what were previously two separate systems:

  1. Focus State - Explicit awaiting states for structured miniapp flows
  2. ConversationContextService - Query enrichment for general conversation

By unifying these, we provide a single source of truth for conversation state, eliminating conflicts and simplifying the codebase.

Why We Built This

The Problem: Multiple Overlapping Systems

Previously, we had separate systems handling different aspects of conversation state:

System Purpose Problem
Focus State Track when waiting for user input Only handled miniapp flows
ConversationContextService Enrich incomplete queries Only handled general conversation
ConversationContinuationDetector Detect if Sage should respond Only for group chats

These systems could conflict: - Focus State might expire while user is still responding - Query enrichment might interfere with miniapp routing - No clear priority when multiple systems want to handle a message

The Solution: Unified State Management

One system with clear priorities:

  1. Explicit Awaiting (highest) - When we're waiting for specific input
  2. Soft Context - For natural follow-ups after actions
  3. Query Enrichment (lowest) - For incomplete queries in general conversation

Architecture

Message arrives at MessageHandler
ConversationStateManager.process_message()
    ┌────────────────────────────────────────────┐
    │ Priority 1: Check Explicit Awaiting        │
    │ ├─ AWAITING_SELECTION (venue choice)       │
    │ ├─ AWAITING_METADATA (notes, tips)         │
    │ ├─ AWAITING_CONFIRMATION (yes/no)          │
    │ └─ AWAITING_INPUT (freeform)               │
    │                                            │
    │ If resolves → Route to miniapp with data   │
    └────────────────────────────────────────────┘
         ↓ (if no resolution)
    ┌────────────────────────────────────────────┐
    │ Priority 2: Check Soft Context             │
    │ ├─ Last action (added_venue, etc.)         │
    │ ├─ Last item (venue ID, name)              │
    │ └─ Context for IntentRouter                │
    │                                            │
    │ Enriches context, doesn't override routing │
    └────────────────────────────────────────────┘
    ┌────────────────────────────────────────────┐
    │ Priority 3: Query Enrichment               │
    │ ├─ Detect incomplete queries               │
    │ ├─ "And tomorrow?" → Full query            │
    │ └─ Enrich with conversation context        │
    │                                            │
    │ Returns enriched query for GPT-5           │
    └────────────────────────────────────────────┘
ProcessingResult {
    route_to: MINIAPP | GPT5
    miniapp_id: Optional[str]
    resolution_data: Optional[Dict]
    enriched_query: str
    soft_context: Dict
}

Key Concepts

Awaiting Types

Type Description TTL Example
SELECTION User must pick from options 2 min "Which Shake Shack? 1. Shibuya 2. Shinjuku"
METADATA User can add info about item 2 min "Added! Any tips for this place?"
CONFIRMATION User must confirm yes/no 2 min "Delete all venues?"
INPUT User must provide freeform text 2 min "What's your trip name?"

Resolution Types

Type Description Example Message
selection User selected from options "2", "in Shibuya", "the Shinjuku one"
metadata User provided metadata "get the burger", "it's cozy"
confirmation User confirmed/denied "yes", "no", "👍"
input User provided freeform input "Tokyo Trip 2024"
cancel User cancelled flow "cancel", "skip", "nevermind"

Soft Context

Soft context is lower-priority state that persists longer (5 min vs 2 min) and provides context for follow-up messages without forcing a specific resolution.

# After adding a venue, set soft context
manager.set_soft_context(
    chat_guid="chat_123",
    context={
        "last_action": "added_venue",
        "last_item_id": "v_abc123",
        "last_item_name": "Shake Shack",
    },
    source_miniapp_id="trip_planner",
)

This helps the system understand context even after explicit awaiting expires.

Usage

For MessageHandler

from app.orchestrator.conversation_state_manager import (
    get_conversation_state_manager,
    RouteTarget,
)

# In handle_message_async()
manager = get_conversation_state_manager()
result = manager.process_message(
    message=request.text,
    chat_guid=chat_id,
    recent_messages=recent_messages,
)

if result.route_to == RouteTarget.MINIAPP:
    # Route to miniapp with resolution data
    await miniapp_router.route_to_miniapp_if_applicable(
        ...,
        focus_resolution=result.resolution_data,
        force_miniapp_id=result.miniapp_id,
    )
    manager.clear_awaiting(chat_id)
else:
    # Use enriched query for GPT-5
    enriched_message = result.enriched_query
    # Continue to normal conversation flow

For MiniApp Handlers

from app.miniapps.routing.base_handler import MiniAppResponse

# When returning options for user selection
return MiniAppResponse(
    message="Which one?\n1. Shibuya\n2. Shinjuku",
    conversation_state={
        "awaiting_type": "awaiting_selection",
        "options": [
            {"name": "Shake Shack", "district": "Shibuya"},
            {"name": "Shake Shack", "district": "Shinjuku"},
        ],
        "context": {
            "original_query": "Shake Shack",
            "selection_type": "venue",
        },
        "source_handler": "trip_planner._add_single_venue",
    },
)

# When venue is added (awaiting metadata + soft context)
return MiniAppResponse(
    message="Added Shake Shack Shibuya! ✓",
    conversation_state={
        "awaiting_type": "awaiting_metadata",
        "context": {
            "target_item_id": venue.id,
            "target_item_name": venue.name,
        },
        "source_handler": "trip_planner._finalize_venue_add",
        "soft_context": {
            "last_action": "added_venue",
            "last_item_id": venue.id,
            "last_item_name": venue.name,
        },
    },
)

Handling Resolution in Handlers

async def handle(self, context: MiniAppContext) -> MiniAppResponse:
    # Check if this is a resolution of an awaiting state
    if context.focus_resolution:
        resolution_type = context.focus_resolution.get("type")

        if resolution_type == "selection":
            selection_index = context.focus_resolution.get("selection_index")
            selected_option = context.focus_resolution.get("selected_option")
            return await self._handle_venue_selection(selection_index)

        elif resolution_type == "metadata":
            metadata_type = context.focus_resolution.get("metadata_type")
            content = context.focus_resolution.get("content")
            target_item_id = context.focus_resolution.get("target_item_id")
            return await self._append_metadata(target_item_id, metadata_type, content)

        elif resolution_type == "confirmation":
            confirmed = context.focus_resolution.get("confirmed")
            return await self._handle_confirmation(confirmed)

    # Normal message handling
    ...

Example Conversation Flows

Flow 1: Venue Selection with Refinement

User: "Shake Shack"
    → TripPlanner finds 3 locations
    → Sets AWAITING_SELECTION with options
    → Returns: "Which one? 1. Shibuya 2. Shinjuku 3. Harajuku"

User: "in Shibuya"
    → ConversationStateManager processes message
    → Matches "Shibuya" to option 1 via refinement
    → Returns ProcessingResult(route_to=MINIAPP, resolution_data={selection_index: 1})
    → MessageHandler routes to TripPlanner with resolution
    → TripPlanner adds venue, sets AWAITING_METADATA + soft context
    → Returns: "Added Shake Shack Shibuya! ✓"

User: "get the shroom burger"
    → ConversationStateManager matches AWAITING_METADATA
    → Detects "get the" pattern → metadata_type: "must_try"
    → Routes to TripPlanner with metadata resolution
    → TripPlanner appends to venue notes
    → Returns: "noted - must try the shroom burger!"

Flow 2: Query Enrichment (No Miniapp)

User: "What's the weather in Tokyo?"
    → No awaiting state, no soft context
    → Not incomplete, passes through
    → Routes to GPT-5

User: "And tomorrow?"
    → No awaiting state
    → Detected as incomplete (follow-up pattern)
    → Enriched: "What's the weather in Tokyo tomorrow?"
    → Routes to GPT-5 with enriched query

Flow 3: Cancel Mid-Flow

User: "add shake shack"
    → TripPlanner finds options
    → Sets AWAITING_SELECTION

User: "nevermind"
    → ConversationStateManager detects cancel pattern
    → Clears awaiting state
    → Returns ProcessingResult(resolution_type=CANCEL)
    → MessageHandler continues to GPT-5
    → GPT-5 responds naturally

Configuration

TTL Values

class ConversationStateManager:
    AWAITING_TTL_SECONDS = 120      # 2 minutes for explicit awaiting
    SOFT_CONTEXT_TTL_SECONDS = 300  # 5 minutes for soft context
    CACHE_TTL_SECONDS = 600         # 10 minutes max cache lifetime

Cancel Patterns

CANCEL_PATTERNS = frozenset([
    "cancel", "skip", "nevermind", "never mind", "nvm",
    "forget it", "forget that", "stop", "quit", "exit",
    "no thanks", "no thank you", "nah", "nope",
    "changed my mind", "actually no", "actually never mind"
])

Metadata Patterns

METADATA_PATTERNS = {
    "must_try": [
        r"(?:get|try|order|have|must have)\s+(?:the\s+)?(.+)",
        r"(?:don'?t\s+miss|can'?t\s+skip)\s+(?:the\s+)?(.+)",
    ],
    "vibe": [
        r"(?:it'?s?|they'?re?|place is)\s+(very\s+)?(\w+)",
        r"(?:cozy|romantic|lively|quiet|chill|fancy|casual)",
    ],
    "best_for": [
        r"(?:great|good|perfect|best|ideal)\s+(?:for|with)\s+(.+)",
    ],
}

Migration from Focus State

If you have existing code using the old focus_state field in MiniAppResponse, it will continue to work (backward compatibility). However, you should migrate to conversation_state:

Old (Deprecated)

return MiniAppResponse(
    message="Which one?",
    focus_state={
        "focus_type": "awaiting_selection",
        "context": {"options": options},
        "source_handler": "handler_name",
    },
)
return MiniAppResponse(
    message="Which one?",
    conversation_state={
        "awaiting_type": "awaiting_selection",
        "options": options,  # Top-level, not nested in context
        "context": {},  # Additional context if needed
        "source_handler": "handler_name",
        "soft_context": {},  # Optional soft context
    },
)

Metrics

The manager tracks metrics for monitoring:

metrics = manager.get_metrics()
# {
#     "total_processed": 1234,
#     "routed_to_miniapp": 456,
#     "routed_to_gpt5": 778,
#     "awaiting_resolutions": 400,
#     "soft_context_hits": 150,
#     "queries_enriched": 200,
#     "cache_hits": 800,
#     "cache_misses": 434,
#     "avg_processing_time_ms": 15.5,
#     "miniapp_route_rate": "37.0%",
#     "enrichment_rate": "16.2%",
#     "cache_hit_rate": "64.8%",
# }

Files Changed

File Change
app/orchestrator/conversation_state_manager.py NEW - Unified manager
app/orchestrator/message_handler.py Uses ConversationStateManager
app/orchestrator/miniapp_router.py Handles conversation_state from responses
app/miniapps/routing/base_handler.py Added conversation_state field
app/miniapps/handlers/trip_planner.py Uses conversation_state instead of focus_state
tests/test_conversation_state_manager.py NEW - Unit tests

ConversationContinuationDetector

  • Still exists, unchanged
  • Purpose: Detect if Sage should respond in group chats
  • Orthogonal to conversation state management

IntentRouter

  • Still exists, unchanged
  • Purpose: Classify TRANSACTIONAL vs CONVERSATIONAL intent
  • Receives soft_context from ConversationStateManager for better classification

MiniAppSession

  • Still exists, unchanged
  • Purpose: Persist miniapp session state across messages
  • ConversationStateManager handles conversation-level state, not session state

Future Enhancements

  1. IntentRouter Integration - Pass soft_context to IntentRouter for better classification when explicit awaiting expires

  2. Proactive Messages - Use soft_context to trigger proactive messages ("Did you try that burger?")

  3. Cross-Session Context - Persist soft_context across sessions for long-term follow-ups

```