"""Card engine — state machine that builds a thought card from conversation.""" from __future__ import annotations from dataclasses import dataclass, field from enum import Enum from distortion_parser import detect_distortions from phase_detector import detect_phase from session import ThoughtCard class CardState(Enum): IDLE = "idle" TRIGGERED = "triggered" DISTORTION_TAGGED = "distortion_tagged" EVIDENCE_GATHERING = "evidence_gathering" REFRAMING = "reframing" COMPLETE = "complete" @dataclass class ActiveCard: """Represents a card being built during conversation.""" state: CardState = CardState.IDLE card: ThoughtCard = field(default_factory=ThoughtCard) turn_count: int = 0 # turns since card was triggered def update_card(active: ActiveCard, model_response: str, user_message: str) -> ActiveCard: """Update the active card based on model response and user message. This is called after each model response. It advances the card state machine. """ active.turn_count += 1 # Detect distortions in model output distortions = detect_distortions(model_response) if distortions: for d in distortions: if d not in active.card.distortions: active.card.distortions.append(d) # Detect phase phase = detect_phase(model_response) # State transitions if active.state == CardState.IDLE: # Card triggers when model reflects back a thought or names a distortion if distortions or phase in ("automatic_thought", "situation"): active.state = CardState.TRIGGERED if user_message and not active.card.automatic_thought: active.card.automatic_thought = _extract_thought(user_message) elif active.state == CardState.TRIGGERED: if distortions: active.state = CardState.DISTORTION_TAGGED elif phase in ("evidence_for", "evidence_against"): active.state = CardState.EVIDENCE_GATHERING elif active.state == CardState.DISTORTION_TAGGED: if phase in ("evidence_for", "evidence_against"): active.state = CardState.EVIDENCE_GATHERING elif active.state == CardState.EVIDENCE_GATHERING: # Accumulate evidence from user messages if phase == "evidence_for" and user_message: active.card.evidence_for.append(user_message[:120]) elif phase == "evidence_against" and user_message: active.card.evidence_against.append(user_message[:120]) elif phase == "reframe": active.state = CardState.REFRAMING elif active.state == CardState.REFRAMING: if user_message and not active.card.balanced_thought: active.card.balanced_thought = user_message[:200] active.state = CardState.COMPLETE return active def should_show_card(active: ActiveCard) -> bool: """Whether the card should be visible in the UI.""" return active.state != CardState.IDLE def reset_card() -> ActiveCard: """Reset to a fresh card.""" return ActiveCard() def _extract_thought(message: str) -> str: """Extract the core thought from a user message (first 150 chars).""" return message[:150].strip()