Conversation State Manager¶
Overview¶
The ConversationStateManager is a unified system for managing conversation state in Archety. It combines what were previously two separate systems:
- Focus State - Explicit awaiting states for structured miniapp flows
- 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:
- Explicit Awaiting (highest) - When we're waiting for specific input
- Soft Context - For natural follow-ups after actions
- 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",
},
)
New (Recommended)¶
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 |
Related Systems¶
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¶
-
IntentRouter Integration - Pass soft_context to IntentRouter for better classification when explicit awaiting expires
-
Proactive Messages - Use soft_context to trigger proactive messages ("Did you try that burger?")
-
Cross-Session Context - Persist soft_context across sessions for long-term follow-ups
```