Conversation State Machine Architecture
Component: Pre-MAF Data Collection Layer
Pattern: Deterministic State Machine (Code-Based)
Purpose: Collects loan application data through conversational UI before triggering MAF Sequential Pipeline
Overview
The ConversationStateMachine is a deterministic, code-based state machine that operates before the Microsoft Agent Framework (MAF) sequential workflow. It manages the conversational data collection phase, tracking user progress through 4 required steps until a complete LoanApplication can be created.
Key Distinction: This is NOT part of the MAF agent workflow. It's a pre-processing layer that collects raw data through natural conversation before handing off to the SequentialPipeline.
Architecture Position
graph TB
User[User Browser]
API[FastAPI Endpoint<br/>/api/chat]
Session[SessionManager<br/>In-Memory Store]
StateMachine[ConversationStateMachine<br/>Deterministic States]
Pipeline[SequentialPipeline<br/>MAF SequentialBuilder]
Agents[4 Specialized Agents<br/>Intake → Credit → Income → Risk]
User -->|POST /api/chat| API
API -->|get_or_create_session| Session
Session -->|state_machine| StateMachine
StateMachine -->|collect data| StateMachine
StateMachine -->|when complete| API
API -->|create LoanApplication| Pipeline
Pipeline -->|process| Agents
style StateMachine fill:#FFF9C4,stroke:#F57F17,stroke-width:3px,color:#000
style Pipeline fill:#E1BEE7,stroke:#4A148C,stroke-width:2px,color:#000
style Session fill:#FFE0B2,stroke:#E65100,stroke-width:2px,color:#000
Flow Separation: 1. Phase 1 (Pre-MAF): ConversationStateMachine collects data 2. Phase 2 (MAF): SequentialPipeline processes complete LoanApplication
State Machine Design
States
class ConversationState(Enum):
INITIAL = "initial" # 0% - Welcome message
HOME_PRICE = "home_price" # 25% - Collect home price
DOWN_PAYMENT = "down_payment" # 50% - Collect down payment %
INCOME = "income" # 75% - Collect annual income
PERSONAL_INFO = "personal_info" # 100% - Collect personal details
PROCESSING = "processing" # Agent workflow in progress
COMPLETE = "complete" # Final decision returned
State Transitions
stateDiagram-v2
[*] --> INITIAL: User arrives
INITIAL --> HOME_PRICE: Provide home price
HOME_PRICE --> DOWN_PAYMENT: Provide down payment %
DOWN_PAYMENT --> INCOME: Provide income range
INCOME --> PERSONAL_INFO: Provide income value
PERSONAL_INFO --> PROCESSING: Submit personal info form
PROCESSING --> COMPLETE: LoanDecision ready
COMPLETE --> [*]: Display results
note right of INITIAL
Pre-scripted responses
No LLM calls
Fast & deterministic
end note
note right of PROCESSING
MAF Sequential Workflow
4 agents + LLM reasoning
Real-time SSE updates
end note
Collected Data Structure
class ConversationStateMachine:
def __init__(self):
self.state = ConversationState.INITIAL
self.collected_data: dict[str, Any] = {
# Collected through quick replies:
"home_price": None, # e.g., "250000"
"down_payment_pct": None, # e.g., "20"
"income_range": None, # e.g., "$60,000-$80,000"
"annual_income": None, # e.g., "72000"
# Collected through form:
"full_name": None, # e.g., "John Doe"
"email": None, # e.g., "john@example.com"
"phone": None, # e.g., "555-1234"
"employment_status": None, # e.g., "employed"
# Calculated:
"loan_amount": None, # home_price - down_payment
}
Session Management & Persistence
In-Memory Session Store
Implementation: apps/api/loan_defenders/api/session_manager.py
class SessionManager:
"""
In-memory session storage for conversation continuity.
Each session contains:
- session_id: UUID
- state_machine: ConversationStateMachine instance
- created_at: Timestamp
- last_activity: Timestamp
"""
def __init__(self):
self._sessions: dict[str, SessionData] = {}
self._lock = asyncio.Lock()
def get_or_create_session(self, session_id: str) -> SessionData:
"""Get existing session or create new one."""
if session_id not in self._sessions:
self._sessions[session_id] = SessionData(
session_id=session_id,
state_machine=ConversationStateMachine(),
created_at=datetime.now(timezone.utc)
)
return self._sessions[session_id]
Persistence Characteristics: - Storage: In-memory dictionary (no database) - Scope: Per-container instance - Lifetime: Until container restart or explicit cleanup - Scaling: Not shared across multiple container instances - TTL: No automatic expiration (manual cleanup if needed)
Trade-offs: - ✅ Fast: No database queries - ✅ Simple: No external dependencies - ✅ Stateless APIs: Each request contains session_id - ❌ Not Persistent: Lost on container restart - ❌ Not Distributed: Each container has separate sessions - ❌ Memory Growth: Sessions accumulate (needs cleanup)
Data Flow Through State Machine
Step-by-Step Data Collection
sequenceDiagram
participant User
participant API
participant Session
participant StateMachine
participant Pipeline
Note over User,StateMachine: Step 1: Initial State
User->>API: POST /api/chat (no data)
API->>Session: get_or_create_session(session_id)
Session-->>API: SessionData with new StateMachine
API->>StateMachine: process_input("")
StateMachine->>StateMachine: state = INITIAL
StateMachine-->>API: Welcome message + home_price quick replies
API-->>User: "Welcome! What's your home price?"
Note over User,StateMachine: Step 2: Home Price
User->>API: POST /api/chat ("250000")
API->>Session: get_or_create_session(session_id)
Session-->>API: Existing SessionData
API->>StateMachine: process_input("250000")
StateMachine->>StateMachine: collected_data["home_price"] = "250000"
StateMachine->>StateMachine: state = DOWN_PAYMENT
StateMachine-->>API: Response + down_payment quick replies
API-->>User: "Great! What's your down payment %?"
Note over User,StateMachine: Step 3: Down Payment
User->>API: POST /api/chat ("20")
API->>StateMachine: process_input("20")
StateMachine->>StateMachine: collected_data["down_payment_pct"] = "20"
StateMachine->>StateMachine: state = INCOME
StateMachine-->>API: Response + income_range quick replies
API-->>User: "Awesome! What's your income range?"
Note over User,StateMachine: Step 4: Income Range
User->>API: POST /api/chat ("$60,000-$80,000")
API->>StateMachine: process_input("$60,000-$80,000")
StateMachine->>StateMachine: collected_data["income_range"] = "$60,000-$80,000"
StateMachine->>StateMachine: state = PERSONAL_INFO
StateMachine-->>API: Response + personal info form
API-->>User: "Almost there! Tell us about yourself"
Note over User,Pipeline: Step 5: Personal Info → MAF Pipeline
User->>API: POST /api/chat (form: name, email, etc.)
API->>StateMachine: process_input(form_data)
StateMachine->>StateMachine: collected_data.update(form_data)
StateMachine->>StateMachine: state = PROCESSING
StateMachine-->>API: application_complete = true
API->>API: Create LoanApplication (Pydantic)
API->>Pipeline: process_application(loan_app)
Note over Pipeline: MAF Sequential Workflow
Pipeline-->>API: Stream ProcessingUpdates (SSE)
API-->>User: Real-time agent progress
Pipeline-->>API: FinalDecisionResponse
API->>StateMachine: Mark COMPLETE
API-->>User: Loan decision with confetti!
Key Design Decisions
1. Why Deterministic State Machine?
Decision: Use code-based state machine instead of LLM-based conversation agent
Rationale: - Predictable UX: Exact same flow every time - Fast: No LLM calls for simple data collection - Cost-Effective: Save AI tokens for complex agent reasoning - Debuggable: State transitions are explicit in code - Form-Friendly: Can use structured UI (forms, quick replies)
Trade-off: Less flexible than LLM-based conversation, but unnecessary for structured data collection
2. Why In-Memory Session Storage?
Decision: Store sessions in memory rather than database
Rationale: - Simplicity: No database setup or queries - Performance: Instant access, no network latency - Scope: Sessions are temporary (only during data collection) - Stateless API: session_id in request, no server-side routing needed
Trade-off: Sessions lost on restart, not shared across containers
3. Why Separate from MAF Pipeline?
Decision: Keep ConversationStateMachine outside of MAF SequentialBuilder
Rationale: - Separation of Concerns: Data collection ≠ agent reasoning - Different Patterns: State machine ≠ sequential agent workflow - Token Efficiency: Don't waste LLM tokens on form filling - Independent Evolution: Can change conversation flow without touching agents
Alternative Considered: Could have made ConversationAgent as first MAF agent, but would waste tokens and add unnecessary complexity
Current Limitations & Future Improvements
Current Limitations
- No Retry Logic
- State machine assumes successful transitions
- No handling for invalid inputs (beyond basic validation)
-
No recovery from partial data loss
-
No Persistence
- Sessions lost on container restart
- No ability to resume conversation after timeout
-
Not suitable for long-running conversations
-
No Multi-Container Support
- Sessions not shared across container instances
- Load balancer must use sticky sessions
-
Horizontal scaling limited
-
No Session Cleanup
- Old sessions accumulate in memory
- No TTL expiration
-
Manual cleanup required
-
No Validation Retry
- Invalid data format causes conversation reset
- No smart error recovery
- User must restart if they provide bad data
Summary
The ConversationStateMachine is a critical pre-MAF component that:
- Collects Data: Gathers loan application data through conversational UI
- Manages State: Tracks progress through 4 required steps
- Validates Input: Ensures data completeness before MAF processing
- Persists Sessions: Stores conversation state in memory (with Redis option)
- Triggers MAF: Creates LoanApplication and starts SequentialPipeline
Key Characteristics: - ✅ Deterministic & fast (no LLM calls) - ✅ Simple in-memory persistence - ✅ Clear separation from MAF workflow - ⚠️ Needs retry logic and validation improvements - ⚠️ Consider Redis for production multi-container deployments
This architecture enables a smooth, predictable user experience for data collection before expensive agent reasoning begins.