Spaces:
Runtime error
Runtime error
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" |