Skip to content

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

  1. No Retry Logic
  2. State machine assumes successful transitions
  3. No handling for invalid inputs (beyond basic validation)
  4. No recovery from partial data loss

  5. No Persistence

  6. Sessions lost on container restart
  7. No ability to resume conversation after timeout
  8. Not suitable for long-running conversations

  9. No Multi-Container Support

  10. Sessions not shared across container instances
  11. Load balancer must use sticky sessions
  12. Horizontal scaling limited

  13. No Session Cleanup

  14. Old sessions accumulate in memory
  15. No TTL expiration
  16. Manual cleanup required

  17. No Validation Retry

  18. Invalid data format causes conversation reset
  19. No smart error recovery
  20. User must restart if they provide bad data

Summary

The ConversationStateMachine is a critical pre-MAF component that:

  1. Collects Data: Gathers loan application data through conversational UI
  2. Manages State: Tracks progress through 4 required steps
  3. Validates Input: Ensures data completeness before MAF processing
  4. Persists Sessions: Stores conversation state in memory (with Redis option)
  5. 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.