Imsachin010's picture
HF Spaces GPU training pipeline
1af4cba
# salespath_env/server/rules.py
from dataclasses import dataclass
from typing import Callable
from ..models import SalesPathAction, SalesPathState
@dataclass
class BusinessRule:
"""
Returns True when the rule is VIOLATED.
"""
rule_id: str
name: str
description: str
check: Callable[[SalesPathState, SalesPathAction], bool]
def _qualify_before_present(
state: SalesPathState,
action: SalesPathAction,
) -> bool:
"""
R01:
PRESENT before QUALIFY is invalid.
"""
if action.action_type == "PRESENT":
return "QUALIFY" not in state.steps_completed
return False
def _demo_before_negotiate(
state: SalesPathState,
action: SalesPathAction,
) -> bool:
"""
R02:
NEGOTIATE before OFFER_DEMO is invalid.
"""
if action.action_type == "NEGOTIATE":
return "OFFER_DEMO" not in state.steps_completed
return False
def _budget_known_to_negotiate(
state: SalesPathState,
action: SalesPathAction,
) -> bool:
"""
R03:
Cannot NEGOTIATE while budget is unknown.
"""
if action.action_type == "NEGOTIATE":
return state.prospect_profile.get("budget_signal") == "unknown"
return False
def _discount_after_objections(
state: SalesPathState,
action: SalesPathAction,
) -> bool:
"""
R04:
Discount only after 2 objections handled.
"""
if action.action_type == "NEGOTIATE":
if "discount" in action.content.lower():
return state.objections_handled < 2
return False
def _no_repeat_action(
state: SalesPathState,
action: SalesPathAction,
) -> bool:
"""
R05:
Same action twice in a row is invalid.
"""
if state.conversation_history:
last_action = state.conversation_history[-1].get("action_type", "")
return last_action == action.action_type
return False
def _prospect_first(
state: SalesPathState,
action: SalesPathAction,
) -> bool:
"""
R06:
First action must be PROSPECT.
"""
if state.turn_number == 1:
return action.action_type != "PROSPECT"
return False
def _followup_timing(
state: SalesPathState,
action: SalesPathAction,
) -> bool:
"""
R07:
FOLLOW_UP only valid after prospect silence.
Violation if the prospect's last response had actual content
(i.e., the prospect is still engaged and waiting for a reply).
"""
if action.action_type == "FOLLOW_UP":
if state.conversation_history:
# Walk backwards to find the last prospect message
for entry in reversed(state.conversation_history):
if entry.get("speaker") == "prospect":
response_token = entry.get("response_token", "")
# FOLLOW_UP is only valid if the prospect went silent
return response_token != "silence"
# No prospect message found — first turn, so violation
return True
# No history at all — first turn, can't FOLLOW_UP yet
return True
return False
def _disqualify_logic(
state: SalesPathState,
action: SalesPathAction,
) -> bool:
"""
R08:
DISQUALIFY only when prospect is genuinely not closeable.
Violation if prospect is actually closeable.
"""
if action.action_type == "DISQUALIFY":
true_budget = state.hidden_state.get("true_budget", 0.5)
close_threshold = state.hidden_state.get("close_threshold", 0.5)
decision_maker = state.prospect_profile.get("decision_maker", True)
return (true_budget >= close_threshold) and decision_maker
return False
def _close_requires_demo(
state: SalesPathState,
action: SalesPathAction,
) -> bool:
"""
R09:
Difficulty 2+ requires OFFER_DEMO before CLOSE.
"""
if action.action_type == "CLOSE":
if state.difficulty >= 2:
return "OFFER_DEMO" not in state.steps_completed
return False
BUSINESS_RULES = [
BusinessRule(
"R01",
"qualify_before_present",
"Must QUALIFY before PRESENT",
_qualify_before_present,
),
BusinessRule(
"R02",
"demo_before_negotiate",
"Must OFFER_DEMO before NEGOTIATE",
_demo_before_negotiate,
),
BusinessRule(
"R03",
"budget_known_to_negotiate",
"Budget must be known before NEGOTIATE",
_budget_known_to_negotiate,
),
BusinessRule(
"R04",
"discount_after_objections",
"Discount only after 2 objections handled",
_discount_after_objections,
),
BusinessRule(
"R05",
"no_repeat_action",
"Cannot repeat same action consecutively",
_no_repeat_action,
),
BusinessRule(
"R06",
"prospect_first",
"First action must be PROSPECT",
_prospect_first,
),
BusinessRule(
"R07",
"followup_timing",
"FOLLOW_UP only after prospect silence",
_followup_timing,
),
BusinessRule(
"R08",
"disqualify_logic",
"DISQUALIFY only when prospect is genuinely unqualified",
_disqualify_logic,
),
BusinessRule(
"R09",
"close_requires_demo",
"Must OFFER_DEMO before CLOSE (difficulty 2+)",
_close_requires_demo,
),
]
def check_rules(
state: SalesPathState,
action: SalesPathAction,
) -> list[str]:
"""
Returns list of violated rule IDs.
"""
violated = []
for rule in BUSINESS_RULES:
if rule.check(state, action):
violated.append(rule.rule_id)
return violated