Token Calculator

This commit is contained in:
2026-06-10 14:28:16 -04:00
parent 64fb83257d
commit 71b98ee4e4
20 changed files with 9719 additions and 916 deletions

View File

@@ -206,7 +206,7 @@ def calculate_sta_benefit(
return _df(rows)
def calculate_bot_deflection_benefit(
def calculate_va_deflection_benefit(
sites: list[SiteInput],
feature_scope: FeatureScope,
scenario: str | Scenario,
@@ -214,41 +214,85 @@ def calculate_bot_deflection_benefit(
params: str = "realistic",
rollout: RolloutPlan | None = None,
) -> pd.DataFrame:
"""Agent labour avoided on calls deflected to Voice Bot / Agentic VA.
"""Agent labour avoided on calls deflected to Voice Bot or Agentic VA.
Not in the original function list but required for a complete net
case — deflected volume never reaches an agent, so the full AHT is
avoided.
**Layered (sequential) deflection model** — Voice Bot runs first on
the full call pool; Agentic VA handles a share of the *residual*
(calls the bot did not deflect). The two mechanisms are substitutes
operating on the same call base, not independent additive benefits.
Effective total deflection:
bot_rate + (1 bot_rate) × va_rate
e.g. 35% + 65% × 15% = 44.75% (not 50%)
**Three realization haircuts** are applied to convert raw deflected
volume into realizable labour savings:
1. ``completion_rate`` — share of "deflected" calls that don't
escalate to an agent mid-session (bot/VA fully handles the call).
2. ``labour_realization`` — staffing flexibility factor; deflected
volume doesn't reduce headcount 1:1 due to minimums, shrinkage,
and occupancy ceilings.
3. ``callback_discount`` — fraction of deflected calls that re-enter
as repeat contacts (poorly-handled deflections drive callbacks).
Combined realistic factor: 0.70 × 0.80 × (1 0.05) ≈ 0.53
The ``params="claim"`` path sets all three factors to their
``claim`` values (1.0 / 1.0 / 0.0) to reproduce the original
Genesys ROI-doc figures for side-by-side comparison.
"""
sc = get_scenario(scenario) if isinstance(scenario, str) else scenario
ro = rollout or NO_ROLLOUT
realization = sc.realization(year)
# Realization haircuts — read from BENEFIT_PARAMS so claim/realistic
# paths are consistent with all other benefit lines.
completion_rate = _param("va_completion_rate", params)
labour_real = _param("va_labour_realization", params)
callback_disc = _param("va_callback_discount", params)
realization_factor = completion_rate * labour_real * (1.0 - callback_disc)
rows = []
for s in sites:
if not feature_scope.active(s.site_name, year):
continue
if feature_scope.feature == "Voice Bot":
deflection = (
# Bot operates on the full call pool.
bot_rate = (
feature_scope.deflection_target
if feature_scope.deflection_target is not None
else sc.voice_bot_deflection
)
deflected_calls = s.voice_volume_monthly * MONTHS_PER_YEAR * bot_rate
else: # Agentic Virtual Agent
deflection = (
# VA operates on the residual after the bot has deflected its share.
# If Voice Bot is not in scope (VA-only deployment), bot_rate = 0
# and the VA works on the full pool — still correct.
bot_rate = sc.voice_bot_deflection
va_rate = (
feature_scope.deflection_target
if feature_scope.deflection_target is not None
else sc.agentic_va_deflection
)
seconds_saved = (
s.voice_volume_monthly * MONTHS_PER_YEAR
* deflection * s.voice_aht_seconds * realization
)
residual_calls = (
s.voice_volume_monthly * MONTHS_PER_YEAR * (1.0 - bot_rate)
)
deflected_calls = residual_calls * va_rate
seconds_saved = deflected_calls * s.voice_aht_seconds * realization
rows.append(
{
"benefit_line": f"{feature_scope.feature} deflection (labour avoided)",
"scope": s.site_name,
"annual_value": seconds_saved * s.agent_cost_per_second
* ro.fraction_live(s.site_name, year),
"annual_value": (
seconds_saved
* s.agent_cost_per_second
* realization_factor
* ro.fraction_live(s.site_name, year)
),
"confidence": Confidence.ESTIMATED.value,
}
)
@@ -320,19 +364,30 @@ def calculate_predictive_routing_benefit(
#: Which calculator handles which feature scope.
#: Agent Copilot and STA exist in named/concurrent variants — both map
#: to the same benefit calculators.
#: Voice Bot and Agentic VA both route to calculate_va_deflection_benefit,
#: which implements the layered sequential model — VA operates on the
#: residual after the bot has deflected its share.
_BENEFIT_DISPATCH = {
"Agent Copilot": (
"Agent Copilot [named]": (
calculate_voice_handle_time_benefit,
calculate_acw_summarization_benefit,
),
"Agent Copilot [concurrent]": (
calculate_voice_handle_time_benefit,
calculate_acw_summarization_benefit,
),
"AI Summary & Insights": (), # benefit carried by Copilot where present
"Speech & Text Analytics": (calculate_sta_benefit,),
"Voice Bot": (calculate_bot_deflection_benefit,),
"Agentic Virtual Agent": (calculate_bot_deflection_benefit,),
"Email AI (Auto-Respond)": (calculate_email_ai_benefit,),
"Speech & Text Analytics [named]": (calculate_sta_benefit,),
"Speech & Text Analytics [concurrent]": (calculate_sta_benefit,),
"Voice Bot": (calculate_va_deflection_benefit,),
"Agentic Virtual Agent": (calculate_va_deflection_benefit,),
"Predictive Routing": (calculate_predictive_routing_benefit,),
}
_COPILOT_FEATURES = {"Agent Copilot [named]", "Agent Copilot [concurrent]"}
def calculate_total_benefit(
sites: list[SiteInput],
@@ -346,10 +401,18 @@ def calculate_total_benefit(
"""All benefit lines for one scenario-year, aggregated per line.
Returns DataFrame: benefit_line, scope, annual_value, confidence.
Voice Bot and Agentic VA deflection benefits use the layered
sequential model: the bot deflects from the full call pool; the VA
deflects from the residual. The two features are NOT additive on
the same base — see :func:`calculate_va_deflection_benefit`.
"""
sc = get_scenario(scenario) if isinstance(scenario, str) else scenario
frames: list[pd.DataFrame] = []
copilot_scope = _scope_for(feature_scopes, "Agent Copilot")
# Find whichever Copilot variant is in scope (named or concurrent).
copilot_scope = next(
(s for s in feature_scopes if s.feature in _COPILOT_FEATURES), None
)
for scope in feature_scopes:
for fn in _BENEFIT_DISPATCH.get(scope.feature, ()): # type: ignore[arg-type]

View File

@@ -7,8 +7,8 @@ Correctness rules implemented here (see spec §4.1):
is enabled at a site, AI Summary & Insights consumption at that site
is forced to zero — Copilot's per-user token rate already includes
interaction summarization. Source: Genesys Cloud AI Experience
tokens FAQ,
https://help.mypurecloud.com/articles/genesys-cloud-ai-experience-tokens-faqs/
token metering,
https://help.genesys.cloud/articles/genesys-cloud-tokens-model/
2. **Token rounding.** Genesys rounds consumption up at billing —
``math.ceil`` is applied to each site's MONTHLY consumption token
total before the rate. Per-user totals (users × tokens/user/month)
@@ -133,12 +133,18 @@ def _monthly_units(site: SiteInput, feature: str, scope: FeatureScope,
site.voice_volume_monthly * deflection * scenario.voice_bot_avg_minutes
) # minutes
if feature == "Agentic Virtual Agent":
deflection = (
# Layered model: VA operates on the residual volume after the voice bot
# has already deflected its share. Cost base = residual × va_rate.
# This is consistent with the benefit model and avoids double-counting
# the same call pool across both deflection mechanisms.
bot_deflection = scenario.voice_bot_deflection
va_deflection = (
scope.deflection_target
if scope.deflection_target is not None
else scenario.agentic_va_deflection
)
return site.voice_volume_monthly * deflection # interactions
residual = site.voice_volume_monthly * (1.0 - bot_deflection)
return residual * va_deflection # interactions
if feature == "Virtual Agent (legacy)":
deflection = scope.deflection_target or 0.0
return site.voice_volume_monthly * deflection
@@ -159,6 +165,11 @@ def _monthly_units(site: SiteInput, feature: str, scope: FeatureScope,
if feature in ("Direct Messaging", "Social Listening", "Social Responses"):
eligibility = scope.eligibility_pct if scope.eligibility_pct is not None else 1.0
return (site.chat_volume_monthly + site.sms_volume_monthly) * eligibility
if feature == "AI Translate":
# Each voice interaction generates one translation; eligibility_pct
# can be used to scope to a subset of interactions (e.g. non-English only).
eligibility = scope.eligibility_pct if scope.eligibility_pct is not None else 1.0
return site.voice_volume_monthly * eligibility # translations
raise KeyError(f"No consumption-volume mapping for feature {feature!r}")
@@ -260,11 +271,12 @@ def calculate_total_cost(
# Rule 1: Agent Copilot covers Supervisor AI Summary. Sites where
# Copilot is active this year are excluded from AI Summary billing —
# Copilot's 40 tokens/user/month already includes summarization.
# https://help.mypurecloud.com/articles/genesys-cloud-ai-experience-tokens-faqs/
# Copilot's per-user token rate already includes interaction summarization.
# https://help.genesys.cloud/articles/genesys-cloud-tokens-model/
_COPILOT_FEATURES = {"Agent Copilot [named]", "Agent Copilot [concurrent]"}
copilot_sites: set[str] = set()
for scope in feature_scopes:
if scope.feature == "Agent Copilot":
if scope.feature in _COPILOT_FEATURES:
copilot_sites |= {
s.site_name for s in sites if scope.active(s.site_name, year)
}

View File

@@ -29,8 +29,8 @@ DEFAULT_DISCOUNT_RATE = 0.08
#: analysis horizon in the P&L. ESTIMATED — confirm with delivery team.
DEFAULT_IMPLEMENTATION_COST = 0.0
_GENESYS_TOKEN_FAQ = (
"https://help.mypurecloud.com/articles/genesys-cloud-ai-experience-tokens-faqs/"
_GENESYS_TOKEN_METERS = (
"https://help.genesys.cloud/articles/genesys-cloud-tokens-model/"
)
# ── Token meters ─────────────────────────────────────────────────────
@@ -41,6 +41,7 @@ _GENESYS_TOKEN_FAQ = (
DEFAULT_METERS: dict[str, TokenMeter] = {
m.feature: m
for m in [
# ── Voice / Bot ───────────────────────────────────────────────
TokenMeter(
feature="Voice Bot",
meter_type=MeterType.PER_MINUTE,
@@ -48,16 +49,26 @@ DEFAULT_METERS: dict[str, TokenMeter] = {
tokens_per_unit=1 / 17, # 0.0588
confidence=Confidence.CONFIRMED,
notes="IVR self-service voice bot minutes; 17 min per token.",
source_url=_GENESYS_TOKEN_FAQ,
source_url=_GENESYS_TOKEN_METERS,
),
TokenMeter(
feature="Digital Bot",
meter_type=MeterType.PER_INTERACTION,
units_per_token=51.0,
tokens_per_unit=1 / 51, # 0.0196
confidence=Confidence.CONFIRMED,
notes="Digital (non-voice) bot sessions; 51 sessions per token.",
source_url=_GENESYS_TOKEN_METERS,
),
# ── Virtual Agent ─────────────────────────────────────────────
TokenMeter(
feature="Virtual Agent (legacy)",
meter_type=MeterType.PER_INTERACTION,
units_per_token=2.0,
tokens_per_unit=0.5,
confidence=Confidence.CONFIRMED,
notes="Legacy (non-agentic) virtual agent; 2 interactions per token.",
source_url=_GENESYS_TOKEN_FAQ,
notes="Legacy (non-agentic) virtual agent; 0.5 tokens per interaction.",
source_url=_GENESYS_TOKEN_METERS,
),
TokenMeter(
feature="Agentic Virtual Agent",
@@ -66,7 +77,42 @@ DEFAULT_METERS: dict[str, TokenMeter] = {
tokens_per_unit=1.2,
confidence=Confidence.CONFIRMED,
notes="Agentic VA; 1.2 tokens per interaction.",
source_url=_GENESYS_TOKEN_FAQ,
source_url=_GENESYS_TOKEN_METERS,
),
# ── Agent Copilot (named vs concurrent) ───────────────────────
TokenMeter(
feature="Agent Copilot [named]",
meter_type=MeterType.PER_USER_PER_MONTH,
units_per_token=0.0,
tokens_per_unit=40.0,
confidence=Confidence.CONFIRMED,
notes=(
"40 tokens per named user per month. Includes interaction "
"summarization (covers AI Summary & Insights)."
),
source_url=_GENESYS_TOKEN_METERS,
),
TokenMeter(
feature="Agent Copilot [concurrent]",
meter_type=MeterType.PER_USER_PER_MONTH,
units_per_token=0.0,
tokens_per_unit=60.0,
confidence=Confidence.CONFIRMED,
notes=(
"60 tokens per concurrent user per month. Includes interaction "
"summarization (covers AI Summary & Insights)."
),
source_url=_GENESYS_TOKEN_METERS,
),
# ── AI Quality / Analytics ────────────────────────────────────
TokenMeter(
feature="AI Scoring",
meter_type=MeterType.PER_INTERACTION,
units_per_token=20.0,
tokens_per_unit=0.05,
confidence=Confidence.CONFIRMED,
notes="AI-scored quality evaluations; 20 evaluations per token.",
source_url=_GENESYS_TOKEN_METERS,
),
TokenMeter(
feature="AI Summary & Insights",
@@ -78,16 +124,50 @@ DEFAULT_METERS: dict[str, TokenMeter] = {
"Supervisor standalone summarization; 50 summaries per token. "
"NOT metered where Agent Copilot is assigned — see cost model."
),
source_url=_GENESYS_TOKEN_FAQ,
source_url=_GENESYS_TOKEN_METERS,
),
# ── Speech & Text Analytics (named vs concurrent) ─────────────
TokenMeter(
feature="Speech & Text Analytics [named]",
meter_type=MeterType.PER_USER_PER_MONTH,
units_per_token=0.0,
tokens_per_unit=30.0,
confidence=Confidence.CONFIRMED,
notes="STA named licence; 30 tokens per named user per month.",
source_url=_GENESYS_TOKEN_METERS,
),
TokenMeter(
feature="Speech & Text Analytics [concurrent]",
meter_type=MeterType.PER_USER_PER_MONTH,
units_per_token=0.0,
tokens_per_unit=45.0,
confidence=Confidence.CONFIRMED,
notes="STA concurrent licence; 45 tokens per concurrent user per month.",
source_url=_GENESYS_TOKEN_METERS,
),
# ── Routing / Engagement ──────────────────────────────────────
TokenMeter(
feature="Predictive Routing",
meter_type=MeterType.PER_INTERACTION,
units_per_token=17.0,
tokens_per_unit=1 / 17, # 0.0588
confidence=Confidence.CONFIRMED,
notes="Predictive routing; 17 routes per token.",
source_url=_GENESYS_TOKEN_METERS,
),
# ── Messaging ─────────────────────────────────────────────────
TokenMeter(
feature="Direct Messaging",
meter_type=MeterType.PER_MESSAGE,
units_per_token=400.0,
tokens_per_unit=0.0025,
confidence=Confidence.CONFIRMED,
notes="FB/IG/WhatsApp messages; 400 messages per token.",
source_url=_GENESYS_TOKEN_FAQ,
notes=(
"Apple Messages for Business, Facebook Messenger, Instagram DM, "
"WhatsApp, and X (Twitter) DM; 400 inbound or outbound messages "
"per token. Additional carrier charges apply for WhatsApp and X."
),
source_url=_GENESYS_TOKEN_METERS,
),
TokenMeter(
feature="Social Listening",
@@ -95,8 +175,8 @@ DEFAULT_METERS: dict[str, TokenMeter] = {
units_per_token=400.0,
tokens_per_unit=0.0025,
confidence=Confidence.CONFIRMED,
notes="400 messages per token.",
source_url=_GENESYS_TOKEN_FAQ,
notes="Genesys Cloud Social; 400 social post ingestions per channel per token.",
source_url=_GENESYS_TOKEN_METERS,
),
TokenMeter(
feature="Social Responses",
@@ -104,53 +184,48 @@ DEFAULT_METERS: dict[str, TokenMeter] = {
units_per_token=400.0,
tokens_per_unit=0.0025,
confidence=Confidence.CONFIRMED,
notes="400 messages per token.",
source_url=_GENESYS_TOKEN_FAQ,
notes="Social Post Responses; 400 outbound messages per channel per token.",
source_url=_GENESYS_TOKEN_METERS,
),
# ── Language / Translation ────────────────────────────────────
TokenMeter(
feature="Speech & Text Analytics",
meter_type=MeterType.PER_USER_PER_MONTH,
units_per_token=0.0, # n/a for per-user meters
tokens_per_unit=30.0,
feature="AI Translate",
meter_type=MeterType.PER_INTERACTION,
units_per_token=2.0,
tokens_per_unit=0.5,
confidence=Confidence.CONFIRMED,
notes="STA: 30 tokens per named user per month.",
source_url=_GENESYS_TOKEN_FAQ,
notes="AI translation; 2 translations per token.",
source_url=_GENESYS_TOKEN_METERS,
),
# ── Genesys Cloud Copilot ─────────────────────────────────────
TokenMeter(
feature="Agent Copilot",
meter_type=MeterType.PER_USER_PER_MONTH,
units_per_token=0.0,
tokens_per_unit=40.0,
feature="Genesys Cloud Copilot",
meter_type=MeterType.PER_INTERACTION,
units_per_token=20.0,
tokens_per_unit=0.05,
confidence=Confidence.CONFIRMED,
notes=(
"40 tokens per named user per month. Includes interaction "
"summarization (covers AI Summary & Insights)."
"20 AI actions per token; Genesys Cloud knowledge queries "
"are not charged."
),
source_url=_GENESYS_TOKEN_FAQ,
source_url=_GENESYS_TOKEN_METERS,
),
# ── Email AI (rates not yet published) ────────────────────────
TokenMeter(
feature="Email AI (Auto-Suggest)",
meter_type=MeterType.PER_USER_PER_MONTH,
units_per_token=0.0,
tokens_per_unit=30.0, # TBD — working default
tokens_per_unit=0.0,
confidence=Confidence.UNKNOWN,
notes="Rate not yet sourced. Working default 30 tokens/user/month.",
notes="Requires Agent Copilot. Token rate not yet published.",
),
TokenMeter(
feature="Email AI (Auto-Respond)",
meter_type=MeterType.PER_MESSAGE,
units_per_token=2.0, # TBD
tokens_per_unit=0.5, # TBD — working default
confidence=Confidence.UNKNOWN,
notes="Rate not yet sourced. Working default 0.5 tokens/message.",
),
TokenMeter(
feature="AI Translate",
meter_type=MeterType.PER_USER_PER_MONTH,
units_per_token=0.0,
tokens_per_unit=20.0, # TBD — working default
tokens_per_unit=0.0,
confidence=Confidence.UNKNOWN,
notes="Rate not yet sourced. Working default 20 tokens/user/month.",
notes="Feature not yet available; rate TBD.",
),
]
}
@@ -342,14 +417,13 @@ CTM_DEFAULT_FEATURE_SCOPES: list[FeatureScope] = [
FeatureScope("Voice Bot", ALL_SITE_NAMES, phase=1, adoption_curve=_RAMP),
FeatureScope("Agentic Virtual Agent", ["NAM", "EMEA"], phase=2,
adoption_curve={2: 0.70, 3: 1.0}),
FeatureScope("Speech & Text Analytics", ALL_SITE_NAMES, phase=1),
FeatureScope("Agent Copilot", ALL_SITE_NAMES, phase=1),
# CTM has named licences — use the [named] variant for both STA and Copilot.
FeatureScope("Speech & Text Analytics [named]", ALL_SITE_NAMES, phase=1),
FeatureScope("Agent Copilot [named]", ALL_SITE_NAMES, phase=1),
FeatureScope("AI Summary & Insights", ALL_SITE_NAMES, phase=1,
adoption_curve=_RAMP),
FeatureScope("Direct Messaging", ALL_SITE_NAMES, phase=1, adoption_curve=_RAMP),
FeatureScope("Email AI (Auto-Suggest)", ["NAM", "EMEA"], phase=2),
FeatureScope("Email AI (Auto-Respond)", ["NAM", "EMEA"], phase=2,
adoption_curve={2: 0.70, 3: 1.0}),
FeatureScope("AI Translate",
["APAC HK", "APAC SG", "APAC SH", "APAC GZ", "APAC JP", "APAC TW"],
phase=3),

View File

@@ -36,9 +36,15 @@ class SiteInput:
voice_acw_seconds: int
fully_loaded_agent_cost_annual: float
fully_loaded_supervisor_cost_annual: float
licence_type: str = "named" # "named" | "concurrent"
languages: list[str] = field(default_factory=list)
def __post_init__(self) -> None:
if self.licence_type not in ("named", "concurrent"):
raise ValueError(
f"{self.site_name}: licence_type must be 'named' or 'concurrent', "
f"got {self.licence_type!r}"
)
if self.agents < 0 or self.supervisors < 0:
raise ValueError(f"{self.site_name}: agent/supervisor counts must be >= 0")
for name in (

View File

@@ -18,12 +18,28 @@ class Scenario:
# ── Cost-side drivers ───────────────────────────────────────────
voice_bot_deflection: float # share of voice volume deflected to bot
voice_bot_avg_minutes: float # bot minutes per deflected call
agentic_va_deflection: float # share of voice volume to agentic VA
# Agentic VA deflection is INCREMENTAL — applied to the residual volume
# after the voice bot has already handled its share (layered model).
# Effective total deflection = bot_rate + (1 bot_rate) × va_rate.
agentic_va_deflection: float # share of RESIDUAL voice volume to agentic VA
voice_summarization_eligibility: float
voice_knowledge_eligibility: float
email_auto_respond_rate: float # share of email auto-responded
email_auto_suggest_acceptance: float
# ── Virtual Agent benefit realization factors ───────────────────
# Applied to both Voice Bot and Agentic VA deflection benefits.
# completion_rate — share of "deflected" calls that don't escalate to an agent
# mid-session (bot/VA fully handles the interaction).
# labour_realization — staffing flexibility: deflected volume doesn't reduce
# headcount 1:1 due to minimums, shrinkage, occupancy ceilings.
# callback_discount — fraction of deflected calls that re-enter as repeat contacts
# (poorly-handled deflections drive callbacks).
# Combined realistic factor: 0.70 × 0.80 × (1 0.05) ≈ 0.53
va_completion_rate: float = 0.70
va_labour_realization: float = 0.80
va_callback_discount: float = 0.05
# year -> fraction of full benefit realized
benefit_realization: dict[int, float] = field(default_factory=dict)
@@ -59,10 +75,16 @@ BENEFIT_PARAMS: dict[str, dict[str, float]] = {
"digital_aht_reduction": {"claim": 0.18, "realistic": 0.085}, # 5-12% Y1
"digital_acw_reduction": {"claim": 1.00, "realistic": 0.40}, # 30-50% Y1
"sta_aht_reduction": {"claim": 0.04, "realistic": 0.015}, # 1-2% Y1
"email_auto_suggest_time_saving": {"claim": 0.30, "realistic": 0.30}, # × acceptance
"email_auto_suggest_time_saving": {"claim": 0.40, "realistic": 0.30}, # × acceptance; Genesys claims 40%
# ESTIMATED lines (no Genesys claim published):
"supervisor_copilot_time_saving": {"claim": 0.10, "realistic": 0.05},
"predictive_routing_aht_reduction": {"claim": 0.04, "realistic": 0.02},
# Virtual Agent realization factors.
# ``claim`` = 100% realization (original model assumption — no haircuts).
# ``realistic`` = production-calibrated midpoints per the spec analysis.
"va_completion_rate": {"claim": 1.00, "realistic": 0.70}, # 60-75% voice bot; 50-70% agentic VA Y1
"va_labour_realization": {"claim": 1.00, "realistic": 0.80}, # 70-85% staffing flexibility
"va_callback_discount": {"claim": 0.00, "realistic": 0.05}, # 5-10% deflected re-enter as repeat contacts
}
@@ -76,6 +98,11 @@ SCENARIOS: dict[str, Scenario] = {
voice_knowledge_eligibility=0.40,
email_auto_respond_rate=0.10,
email_auto_suggest_acceptance=0.25,
# VA realization: conservative — low completion, limited staffing flex
# Combined: 0.60 × 0.70 × (1 0.05) ≈ 0.40
va_completion_rate=0.60,
va_labour_realization=0.70,
va_callback_discount=0.05,
benefit_realization={1: 0.30, 2: 0.60, 3: 0.80},
),
"realistic": Scenario(
@@ -87,6 +114,11 @@ SCENARIOS: dict[str, Scenario] = {
voice_knowledge_eligibility=0.60,
email_auto_respond_rate=0.20,
email_auto_suggest_acceptance=0.40,
# VA realization: production midpoints per spec analysis
# Combined: 0.70 × 0.80 × (1 0.05) ≈ 0.53
va_completion_rate=0.70,
va_labour_realization=0.80,
va_callback_discount=0.05,
benefit_realization={1: 0.50, 2: 0.80, 3: 0.95},
),
"stretch": Scenario(
@@ -98,6 +130,11 @@ SCENARIOS: dict[str, Scenario] = {
voice_knowledge_eligibility=0.80,
email_auto_respond_rate=0.50,
email_auto_suggest_acceptance=0.60,
# VA realization: optimistic — high completion, good staffing flexibility
# Combined: 0.75 × 0.85 × (1 0.03) ≈ 0.62
va_completion_rate=0.75,
va_labour_realization=0.85,
va_callback_discount=0.03,
benefit_realization={1: 0.75, 2: 0.95, 3: 1.00},
),
}