File size: 3,232 Bytes
4ae4ae8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
"""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()