File size: 4,605 Bytes
b77d3c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# salespath_env/server/prospect_simulator.py

from ..models import SalesPathAction, SalesPathState


RESPONSE_TEXT = {
    "open:positive_signal": "That sounds interesting. Tell me more about how this works.",
    "open:neutral_signal": "I see. We're evaluating a few options at the moment.",

    "objection:price": "The pricing seems higher than what we budgeted for.",
    "objection:timing": "The timing isn't ideal — we're in the middle of a quarter close.",
    "objection:premature_pitch": (
        "I'm not sure we're ready to discuss solutions yet. "
        "What do you know about our current situation?"
    ),

    "deflect:budget_not_discussed": (
        "We haven't really talked about what we're looking for yet."
    ),
    "deflect:stall": (
        "Let me get back to you on this. A lot is happening on our end."
    ),

    "accept:demo_scheduled": (
        "Yes, let's set up a demo. What time works next week?"
    ),
    "accept:close_success": (
        "Alright, I think we can move forward with this. "
        "Send over the paperwork."
    ),

    "reject:close_failed": (
        "I don't think we're ready to commit at this point."
    ),

    "silence": "",

    "exit:disqualified": (
        "I think we're done here. This isn't the right fit."
    ),
}


class ProspectSimulator:
    """
    Pure rule-based simulator.
    No LLM. No transformers. Deterministic behavior.
    """

    def respond(
        self,
        action: SalesPathAction,
        state: SalesPathState,
    ) -> tuple[str, str]:
        """
        Returns:
            (response_token, response_text)
        """

        token = self._get_token(action, state)
        text = RESPONSE_TEXT[token]

        return token, text

    def _get_token(
        self,
        action: SalesPathAction,
        state: SalesPathState,
    ) -> str:
        atype = action.action_type
        difficulty = state.difficulty
        turn = state.turn_number
        profile = state.prospect_profile
        hidden = state.hidden_state
        objections = state.objections_handled

        # -----------------------------
        # Rule-triggered responses first
        # -----------------------------

        if state.constraints_violated:
            latest = state.constraints_violated[-1]

            if latest == "R01":
                return "objection:premature_pitch"

            if latest == "R03":
                return "deflect:budget_not_discussed"

        # -----------------------------
        # Action-based responses
        # -----------------------------

        if atype == "PROSPECT":
            return "open:positive_signal"

        if atype == "QUALIFY":
            # Reveal budget if hidden
            if profile.get("budget_signal") == "unknown":
                state.prospect_profile["budget_signal"] = hidden.get(
                    "revealed_budget",
                    "medium",
                )

            return "open:neutral_signal"

        if atype == "PRESENT":
            if difficulty >= 2:
                if objections == 0:
                    return "objection:price"

            return "open:positive_signal"

        if atype == "HANDLE_OBJECTION":
            state.objections_handled += 1

            required_objections = hidden.get("num_objections", 1)

            if state.objections_handled >= required_objections:
                return "open:positive_signal"

            if objections == 0:
                return "objection:timing"

            return "open:positive_signal"

        if atype == "OFFER_DEMO":
            return "accept:demo_scheduled"

        if atype == "NEGOTIATE":
            return "open:neutral_signal"

        if atype == "CLOSE":
            true_budget = hidden.get("true_budget", 0.7)
            close_threshold = hidden.get("close_threshold", 0.5)
            decision_maker = profile.get("decision_maker", True)

            if (
                true_budget >= close_threshold
                and decision_maker
            ):
                return "accept:close_success"

            return "reject:close_failed"

        if atype == "FOLLOW_UP":
            return "open:neutral_signal"

        if atype == "DISQUALIFY":
            return "exit:disqualified"

        # -----------------------------
        # Difficulty 3+ mode shift
        # -----------------------------

        if difficulty >= 3 and turn >= 10:
            import random

            if random.random() < hidden.get("stall_probability", 0.0):
                return "deflect:stall"

        return "open:neutral_signal"