File size: 17,450 Bytes
e6a02dd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# salespath_env/server/task_bank.py
"""
Prospect profiles, organised by difficulty.

Per arXiv:2408.10215 §3 ("Reward shaping cannot fix data scarcity"),
the training distribution must be wide enough that the policy cannot
overfit to a handful of memorised episodes.  We expand to ~20 profiles
per level and reserve the last 4 of each level as a held-out eval set.

Public API
----------
    sample_profile(difficulty, split="train", rng=None)
        Sample a profile for online training/eval.

    iter_eval_profiles(difficulty)
        Iterate over the held-out eval profiles.

    iter_train_profiles(difficulty)
        Iterate over the training profiles.
"""

from __future__ import annotations

import random
from dataclasses import dataclass
from typing import Iterator, List, Optional


@dataclass
class ProspectProfile:
    company_name: str
    company_size: str           # small / medium / enterprise
    industry: str
    budget_signal: str          # high / medium / low / unknown
    pain_points: List[str]
    decision_maker: bool

    # Hidden values — never exposed directly to the agent.
    true_budget: float          # 0.0 → 1.0
    close_threshold: float
    stall_probability: float


# ---------------------------------------------------------------------------
# LEVEL 1 — Easy
# Budget known, decision maker present, close should succeed.
# ---------------------------------------------------------------------------

PROFILES_L1: List[ProspectProfile] = [
    ProspectProfile("Meridian Retail",  "medium",     "retail",            "high",   ["manual inventory tracking", "slow reporting"],            True, 0.80, 0.50, 0.0),
    ProspectProfile("Northline Foods",  "small",      "food distribution", "medium", ["supplier delays", "inventory mismatch"],                  True, 0.60, 0.50, 0.0),
    ProspectProfile("Crestline Auto",   "medium",     "automotive parts",  "high",   ["parts forecasting", "warehouse turnover"],                True, 0.75, 0.50, 0.0),
    ProspectProfile("HarborGoods",      "small",      "consumer goods",    "high",   ["channel reporting", "stockout alerts"],                   True, 0.72, 0.50, 0.0),
    ProspectProfile("Ironclad Tools",   "medium",     "industrial supply", "high",   ["catalog updates", "B2B quoting"],                         True, 0.78, 0.50, 0.0),
    ProspectProfile("Greenway Grocer",  "medium",     "grocery",           "medium", ["expiration tracking", "cold-chain visibility"],           True, 0.62, 0.50, 0.0),
    ProspectProfile("BlueRiver Pharma", "medium",     "pharmacy retail",   "high",   ["compliance forms", "expiry alerts"],                      True, 0.70, 0.50, 0.0),
    ProspectProfile("Stride Apparel",   "small",      "apparel",           "medium", ["sizing variants", "returns workflow"],                    True, 0.58, 0.50, 0.0),
    ProspectProfile("Summit Hardware",  "medium",     "hardware retail",   "high",   ["SKU bloat", "POS integration"],                           True, 0.74, 0.50, 0.0),
    ProspectProfile("Pinecrest Books",  "small",      "books",             "medium", ["seasonal demand", "inventory shrinkage"],                 True, 0.55, 0.50, 0.0),
    ProspectProfile("Lakeside Resort",  "medium",     "hospitality",       "high",   ["guest preference data", "F&B inventory"],                 True, 0.68, 0.50, 0.0),
    ProspectProfile("Granite Coffee",   "small",      "F&B chain",         "medium", ["multi-location SKU sync", "shrinkage"],                   True, 0.60, 0.50, 0.0),
    ProspectProfile("Horizon Outdoor",  "medium",     "sporting goods",    "high",   ["seasonal kitting", "regional demand"],                    True, 0.71, 0.50, 0.0),
    ProspectProfile("Cobalt Components","medium",     "electronics dist.", "high",   ["BOM management", "lead-time variance"],                   True, 0.77, 0.50, 0.0),
    ProspectProfile("Verdant Garden",   "small",      "garden centre",     "medium", ["seasonal stock", "weather-driven demand"],                True, 0.56, 0.50, 0.0),
    # ---- eval split (last 4) -----------------------------------------------
    ProspectProfile("Falcon Sports",    "medium",     "sporting goods",    "high",   ["return rate spikes", "regional sizing"],                  True, 0.69, 0.50, 0.0),
    ProspectProfile("Maple & Co",       "small",      "specialty grocery", "medium", ["organic inventory", "seasonal sourcing"],                 True, 0.57, 0.50, 0.0),
    ProspectProfile("Skyline Pet",      "medium",     "pet supplies",      "high",   ["food expiration", "subscription kits"],                   True, 0.73, 0.50, 0.0),
    ProspectProfile("Helix Beauty",     "small",      "beauty retail",     "medium", ["palette variants", "promo windows"],                      True, 0.61, 0.50, 0.0),
]


# ---------------------------------------------------------------------------
# LEVEL 2 — Medium
# Budget hidden initially, one objection expected, demo required for close.
# ---------------------------------------------------------------------------

PROFILES_L2: List[ProspectProfile] = [
    ProspectProfile("Apex Logistics",   "enterprise", "logistics",         "unknown", ["route optimization", "driver coordination", "fuel tracking"], True, 0.70, 0.50, 0.0),
    ProspectProfile("Vertex Supply",    "medium",     "manufacturing",     "unknown", ["vendor visibility", "purchase delays"],                       True, 0.55, 0.50, 0.0),
    ProspectProfile("Polaris Freight",  "enterprise", "freight",           "unknown", ["dispatch SLA", "fleet maintenance"],                          True, 0.66, 0.50, 0.0),
    ProspectProfile("Cobra Builders",   "medium",     "construction",      "unknown", ["project costing", "subcontractor coordination"],              True, 0.60, 0.50, 0.0),
    ProspectProfile("Aegis Energy",     "enterprise", "utilities",         "unknown", ["asset uptime", "grid analytics"],                             True, 0.71, 0.50, 0.0),
    ProspectProfile("Crystal Foods",    "medium",     "food processing",   "unknown", ["batch traceability", "regulatory reporting"],                 True, 0.58, 0.50, 0.0),
    ProspectProfile("Atlas Steel",      "enterprise", "metals",            "unknown", ["yield optimization", "downtime reduction"],                   True, 0.65, 0.50, 0.0),
    ProspectProfile("Quartz Mobility",  "medium",     "mobility tech",     "unknown", ["fleet utilization", "telematics ingest"],                     True, 0.59, 0.50, 0.0),
    ProspectProfile("Beacon Insure",    "enterprise", "insurance",         "unknown", ["claims triage", "fraud signals"],                             True, 0.72, 0.50, 0.0),
    ProspectProfile("Tesseract Bio",    "medium",     "biotech",           "unknown", ["lab inventory", "experiment tracking"],                       True, 0.62, 0.50, 0.0),
    ProspectProfile("Pivot Media",      "enterprise", "media",             "unknown", ["content rights", "campaign attribution"],                     True, 0.69, 0.50, 0.0),
    ProspectProfile("Solstice Travel",  "medium",     "travel",            "unknown", ["booking variance", "supplier API churn"],                     True, 0.57, 0.50, 0.0),
    ProspectProfile("Anvil Robotics",   "enterprise", "robotics",          "unknown", ["fleet calibration", "OTA updates"],                           True, 0.74, 0.50, 0.0),
    ProspectProfile("Pacific Marine",   "medium",     "shipping",          "unknown", ["port turnaround", "container visibility"],                    True, 0.61, 0.50, 0.0),
    ProspectProfile("Lumen Telecom",    "enterprise", "telecom",           "unknown", ["service incidents", "field tech routing"],                    True, 0.68, 0.50, 0.0),
    # ---- eval split --------------------------------------------------------
    ProspectProfile("Onyx Logistics",   "enterprise", "logistics",         "unknown", ["last-mile delays", "warehouse handoffs"],                     True, 0.67, 0.50, 0.0),
    ProspectProfile("Sigma Industrial", "medium",     "industrial",        "unknown", ["MRO inventory", "supplier OTIF"],                             True, 0.56, 0.50, 0.0),
    ProspectProfile("Kepler Insurance", "enterprise", "insurance",         "unknown", ["renewal forecasting", "policy ops"],                          True, 0.70, 0.50, 0.0),
    ProspectProfile("Mosaic Energy",    "enterprise", "energy",            "unknown", ["asset health", "predictive maintenance"],                     True, 0.66, 0.50, 0.0),
]


# ---------------------------------------------------------------------------
# LEVEL 3 — Hard
# Budget hidden, two objections, possible stalling, decision maker may be absent.
# ---------------------------------------------------------------------------

PROFILES_L3: List[ProspectProfile] = [
    ProspectProfile("Nova Financial",    "enterprise", "finance",       "unknown", ["compliance reporting", "audit trails", "data silos"],      False, 0.60, 0.55, 0.30),
    ProspectProfile("Atlas Health",      "enterprise", "healthcare",    "unknown", ["patient workflow delays", "reporting compliance"],         False, 0.65, 0.55, 0.25),
    ProspectProfile("Citadel Bank",      "enterprise", "banking",       "unknown", ["KYC automation", "fraud detection lag"],                   False, 0.62, 0.55, 0.30),
    ProspectProfile("Helios Hospitals",  "enterprise", "healthcare",    "unknown", ["EHR fragmentation", "billing reconciliation"],             False, 0.58, 0.55, 0.30),
    ProspectProfile("Orion Asset Mgmt",  "enterprise", "asset mgmt",    "unknown", ["risk reporting", "ESG data ingestion"],                    False, 0.66, 0.55, 0.25),
    ProspectProfile("Sable Pharma",      "enterprise", "pharma",        "unknown", ["GxP traceability", "trial data integrity"],                False, 0.61, 0.55, 0.30),
    ProspectProfile("Magellan Travel",   "enterprise", "travel ops",    "unknown", ["disruption response", "loyalty data"],                     False, 0.59, 0.55, 0.30),
    ProspectProfile("Crucible Defense",  "enterprise", "defense",       "unknown", ["clearance workflow", "supply chain audit"],                False, 0.63, 0.55, 0.25),
    ProspectProfile("Seraphim Care",     "enterprise", "elder care",    "unknown", ["caregiver scheduling", "regulatory reporting"],            False, 0.57, 0.55, 0.30),
    ProspectProfile("Polaris Reinsure",  "enterprise", "reinsurance",   "unknown", ["catastrophe modeling", "loss aggregation"],                False, 0.64, 0.55, 0.30),
    ProspectProfile("Vanguard Edu",      "enterprise", "education",     "unknown", ["enrollment ops", "compliance audits"],                     False, 0.55, 0.55, 0.25),
    ProspectProfile("Aurora Telecom",    "enterprise", "telecom",       "unknown", ["spectrum analytics", "tower asset mgmt"],                  False, 0.60, 0.55, 0.30),
    ProspectProfile("Trident Marine",    "enterprise", "marine",        "unknown", ["fleet compliance", "fuel arbitrage"],                      False, 0.58, 0.55, 0.30),
    ProspectProfile("Granite Mining",    "enterprise", "mining",        "unknown", ["asset uptime", "ESG reporting"],                           False, 0.62, 0.55, 0.30),
    ProspectProfile("Echelon Health",    "enterprise", "health-ins",    "unknown", ["claims adjudication", "provider network"],                 False, 0.59, 0.55, 0.30),
    # ---- eval split --------------------------------------------------------
    ProspectProfile("Castle Securities", "enterprise", "securities",    "unknown", ["trade surveillance", "settlement breaks"],                 False, 0.61, 0.55, 0.30),
    ProspectProfile("Lighthouse Care",   "enterprise", "elder care",    "unknown", ["staffing variance", "incident reporting"],                 False, 0.56, 0.55, 0.25),
    ProspectProfile("Crown Reinsurance", "enterprise", "reinsurance",   "unknown", ["catastrophe modeling", "treaty management"],               False, 0.63, 0.55, 0.30),
    ProspectProfile("Apex Pharma",       "enterprise", "pharma",        "unknown", ["clinical-trial reporting", "supply chain audit"],          False, 0.60, 0.55, 0.30),
]


# ---------------------------------------------------------------------------
# LEVEL 4 — Adversarial
# Misleading "high" budget signal but actual budget < threshold,
# OR decision maker absent.  Correct action is DISQUALIFY.
# ---------------------------------------------------------------------------

PROFILES_L4: List[ProspectProfile] = [
    ProspectProfile("Cipher Tech",        "small", "technology",        "high", ["security", "compliance"],                              False, 0.20, 0.50, 0.50),
    ProspectProfile("BluePeak Studio",    "small", "creative agency",   "high", ["project visibility", "client reporting"],              False, 0.25, 0.50, 0.40),
    ProspectProfile("Nimbus Labs",        "small", "research",          "high", ["grant reporting", "experiment tracking"],              False, 0.18, 0.50, 0.45),
    ProspectProfile("Halo Consulting",    "small", "consulting",        "high", ["billable utilization", "client deliverables"],         False, 0.22, 0.50, 0.45),
    ProspectProfile("Spire Architects",   "small", "architecture",      "high", ["drawing revisions", "permit tracking"],                False, 0.24, 0.50, 0.40),
    ProspectProfile("Quill Publishing",   "small", "publishing",        "high", ["royalty tracking", "rights management"],               False, 0.17, 0.50, 0.50),
    ProspectProfile("Onyx Boutique",      "small", "fashion boutique",  "high", ["trend forecasting", "supplier mix"],                   False, 0.21, 0.50, 0.45),
    ProspectProfile("Topaz Cinema",       "small", "indie film",        "high", ["distribution rights", "festival logistics"],           False, 0.19, 0.50, 0.50),
    ProspectProfile("Mariner Charter",    "small", "yacht charter",     "high", ["seasonal demand", "crew scheduling"],                  False, 0.23, 0.50, 0.45),
    ProspectProfile("Velvet Catering",    "small", "catering",          "high", ["event variance", "ingredient costing"],                False, 0.16, 0.50, 0.50),
    ProspectProfile("Echo Photography",   "small", "studio",            "high", ["project pipelines", "asset licensing"],                False, 0.20, 0.50, 0.45),
    ProspectProfile("Stellar Wellness",   "small", "wellness",          "high", ["membership churn", "class scheduling"],                False, 0.22, 0.50, 0.45),
    ProspectProfile("Drift Digital",      "small", "agency",            "high", ["campaign attribution", "creative asset library"],      False, 0.19, 0.50, 0.50),
    ProspectProfile("Ember Theater",      "small", "performing arts",   "high", ["production budgeting", "ticket allocation"],           False, 0.18, 0.50, 0.45),
    ProspectProfile("Halcyon Crafts",     "small", "artisan retail",    "high", ["maker payouts", "fulfilment SLA"],                     False, 0.21, 0.50, 0.50),
    # ---- eval split --------------------------------------------------------
    ProspectProfile("Onyx Tech",          "small", "technology",        "high", ["zero-trust rollout", "compliance"],                    False, 0.19, 0.50, 0.50),
    ProspectProfile("Haven Studio",       "small", "creative agency",   "high", ["client-asset versioning", "billing transparency"],     False, 0.23, 0.50, 0.40),
    ProspectProfile("Beacon Indie",       "small", "publishing",        "high", ["distribution rights", "royalty splits"],               False, 0.17, 0.50, 0.50),
    ProspectProfile("Kindled Catering",   "small", "catering",          "high", ["event variance", "menu engineering"],                  False, 0.22, 0.50, 0.45),
]


# ---------------------------------------------------------------------------
# Splits
# ---------------------------------------------------------------------------

# Last `_EVAL_SIZE` of each list is the held-out eval split.
_EVAL_SIZE = 4


def _split(profiles: List[ProspectProfile]) -> tuple[list, list]:
    return profiles[:-_EVAL_SIZE], profiles[-_EVAL_SIZE:]


_TRAIN_L1, _EVAL_L1 = _split(PROFILES_L1)
_TRAIN_L2, _EVAL_L2 = _split(PROFILES_L2)
_TRAIN_L3, _EVAL_L3 = _split(PROFILES_L3)
_TRAIN_L4, _EVAL_L4 = _split(PROFILES_L4)


TRAIN_PROFILES = {1: _TRAIN_L1, 2: _TRAIN_L2, 3: _TRAIN_L3, 4: _TRAIN_L4}
EVAL_PROFILES  = {1: _EVAL_L1,  2: _EVAL_L2,  3: _EVAL_L3,  4: _EVAL_L4}
ALL_PROFILES   = {1: PROFILES_L1, 2: PROFILES_L2, 3: PROFILES_L3, 4: PROFILES_L4}


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

def sample_profile(
    difficulty: int,
    split: str = "train",
    rng: Optional[random.Random] = None,
) -> ProspectProfile:
    """
    Sample one profile from the requested split.

    Parameters
    ----------
    difficulty : int (1..4)
    split      : "train" | "eval" | "all"
    rng        : optional pre-seeded RNG for reproducibility
    """
    if difficulty not in TRAIN_PROFILES:
        difficulty = 1

    pool: List[ProspectProfile]
    if split == "eval":
        pool = EVAL_PROFILES[difficulty]
    elif split == "all":
        pool = ALL_PROFILES[difficulty]
    else:
        pool = TRAIN_PROFILES[difficulty]

    return (rng or random).choice(pool)


def iter_train_profiles(difficulty: int) -> Iterator[ProspectProfile]:
    yield from TRAIN_PROFILES[difficulty]


def iter_eval_profiles(difficulty: int) -> Iterator[ProspectProfile]:
    yield from EVAL_PROFILES[difficulty]