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

File diff suppressed because one or more lines are too long

View File

@@ -8,9 +8,11 @@ from tokencalc.benefit_model import (
calculate_acw_summarization_benefit,
calculate_email_ai_benefit,
calculate_total_benefit,
calculate_va_deflection_benefit,
)
from tokencalc.defaults import CTM_DEFAULT_FEATURE_SCOPES, CTM_DEFAULT_SITES
from tokencalc.inputs import WORKING_SECONDS_PER_YEAR, FeatureScope, SiteInput
from tokencalc.scenarios import BENEFIT_PARAMS
ALL_SITES = [s.site_name for s in CTM_DEFAULT_SITES]
@@ -105,3 +107,133 @@ def test_zero_volume_site_is_safe():
def test_working_seconds_constant():
assert WORKING_SECONDS_PER_YEAR == 2_080 * 3_600
# ── Virtual Agent deflection tests ───────────────────────────────────────────
def test_va_bot_deflection_hand_check():
"""Voice Bot: 10,000 calls/mo × 12 × 35% bot_rate × 300s AHT
× 50% Y1 realization × realization_factor × $0.01/s.
realistic realization_factor = 0.70 × 0.80 × (1 0.05) = 0.532
"""
site = _small_site()
df = calculate_va_deflection_benefit(
[site],
FeatureScope("Voice Bot", ["Small"], deflection_target=0.35),
"realistic",
year=1,
params="realistic",
)
completion = BENEFIT_PARAMS["va_completion_rate"]["realistic"]
labour = BENEFIT_PARAMS["va_labour_realization"]["realistic"]
callback = BENEFIT_PARAMS["va_callback_discount"]["realistic"]
real_factor = completion * labour * (1.0 - callback)
expected = (
10_000 * 12 # annual calls
* 0.35 # bot deflection rate
* 300 # AHT seconds
* 0.50 # Y1 scenario realization
* real_factor # completion × labour × (1 callback)
* 0.01 # labour rate per second
)
assert df["annual_value"].sum() == pytest.approx(expected)
def test_va_agentic_deflection_uses_residual():
"""Agentic VA must operate on the residual (1 bot_rate) call pool,
not the full volume.
With bot_rate=0.35 and va_rate=0.15:
residual = 10,000 × (1 0.35) = 6,500 calls/mo
va_deflected = 6,500 × 0.15 = 975 calls/mo
"""
site = _small_site()
df = calculate_va_deflection_benefit(
[site],
FeatureScope("Agentic Virtual Agent", ["Small"], deflection_target=0.15),
"realistic",
year=1,
params="realistic",
)
completion = BENEFIT_PARAMS["va_completion_rate"]["realistic"]
labour = BENEFIT_PARAMS["va_labour_realization"]["realistic"]
callback = BENEFIT_PARAMS["va_callback_discount"]["realistic"]
real_factor = completion * labour * (1.0 - callback)
# realistic scenario: voice_bot_deflection = 0.35
bot_rate = 0.35
va_rate = 0.15
expected = (
10_000 * 12 # annual calls
* (1.0 - bot_rate) * va_rate # residual × va_rate (layered)
* 300 # AHT seconds
* 0.50 # Y1 scenario realization
* real_factor
* 0.01
)
assert df["annual_value"].sum() == pytest.approx(expected)
def test_va_no_double_count():
"""Combined bot + VA benefit must be less than the naive additive sum.
Naive (wrong): volume × (bot_rate + va_rate) × AHT × ...
Correct (layered): volume × (bot_rate + (1bot_rate)×va_rate) × AHT × ...
With bot=35%, va=15%:
naive total deflection = 50%
layered total deflection = 35% + 65%×15% = 44.75%
"""
site = _small_site()
bot_scope = FeatureScope("Voice Bot", ["Small"], deflection_target=0.35)
va_scope = FeatureScope("Agentic Virtual Agent", ["Small"], deflection_target=0.15)
bot_df = calculate_va_deflection_benefit([site], bot_scope, "realistic", year=1)
va_df = calculate_va_deflection_benefit([site], va_scope, "realistic", year=1)
combined = bot_df["annual_value"].sum() + va_df["annual_value"].sum()
# Naive additive (the old broken model): both on full volume
completion = BENEFIT_PARAMS["va_completion_rate"]["realistic"]
labour = BENEFIT_PARAMS["va_labour_realization"]["realistic"]
callback = BENEFIT_PARAMS["va_callback_discount"]["realistic"]
real_factor = completion * labour * (1.0 - callback)
naive = (
10_000 * 12 * (0.35 + 0.15) * 300 * 0.50 * real_factor * 0.01
)
assert combined < naive, (
f"Combined layered benefit ({combined:.2f}) should be less than "
f"naive additive ({naive:.2f}) — double-count not fixed"
)
# Also verify the exact layered total
layered_deflection = 0.35 + (1.0 - 0.35) * 0.15 # = 0.4475
expected_combined = (
10_000 * 12 * layered_deflection * 300 * 0.50 * real_factor * 0.01
)
assert combined == pytest.approx(expected_combined)
def test_va_claim_params_reproduce_no_haircut():
"""params='claim' must apply zero haircuts (all factors = 1.0),
reproducing the original Genesys ROI-doc assumption."""
site = _small_site()
df_claim = calculate_va_deflection_benefit(
[site],
FeatureScope("Voice Bot", ["Small"], deflection_target=0.35),
"realistic",
year=1,
params="claim",
)
df_realistic = calculate_va_deflection_benefit(
[site],
FeatureScope("Voice Bot", ["Small"], deflection_target=0.35),
"realistic",
year=1,
params="realistic",
)
# claim should be strictly higher (no haircuts applied)
assert df_claim["annual_value"].sum() > df_realistic["annual_value"].sum()
# claim realization_factor = 1.0 × 1.0 × (1 0.0) = 1.0
expected_claim = 10_000 * 12 * 0.35 * 300 * 0.50 * 1.0 * 0.01
assert df_claim["annual_value"].sum() == pytest.approx(expected_claim)

View File

@@ -111,7 +111,7 @@ def test_scenario_json_roundtrip(tmp_path):
scenario_state_to_json(
CTM_DEFAULT_SITES, CTM_DEFAULT_TAKEOUTS, CTM_DEFAULT_FEATURE_SCOPES, p
)
sites, takeouts, scopes = scenario_state_from_json(p)
sites, takeouts, scopes, _rollout = scenario_state_from_json(p)
assert [s.site_name for s in sites] == [s.site_name for s in CTM_DEFAULT_SITES]
assert takeouts[0].annual_cost == CTM_DEFAULT_TAKEOUTS[0].annual_cost
assert scopes[0].adoption_curve == CTM_DEFAULT_FEATURE_SCOPES[0].adoption_curve

View File

@@ -34,8 +34,8 @@ def test_default_sites_match_contracted_users():
def test_sta_acceptance_number():
"""2,088 users × 30 tokens × 12 months × $1 = $751,680."""
df = calculate_per_user_ai_cost(
CTM_DEFAULT_SITES, _scope("Speech & Text Analytics"),
DEFAULT_METERS["Speech & Text Analytics"], DEFAULT_PRICING,
CTM_DEFAULT_SITES, _scope("Speech & Text Analytics [named]"),
DEFAULT_METERS["Speech & Text Analytics [named]"], DEFAULT_PRICING,
)
assert df["annual_cost"].sum() == pytest.approx(751_680)
@@ -43,16 +43,20 @@ def test_sta_acceptance_number():
def test_agent_copilot_acceptance_number():
"""2,088 users × 40 tokens × 12 months × $1 = $1,002,240."""
df = calculate_per_user_ai_cost(
CTM_DEFAULT_SITES, _scope("Agent Copilot"),
DEFAULT_METERS["Agent Copilot"], DEFAULT_PRICING,
CTM_DEFAULT_SITES, _scope("Agent Copilot [named]"),
DEFAULT_METERS["Agent Copilot [named]"], DEFAULT_PRICING,
)
assert df["annual_cost"].sum() == pytest.approx(1_002_240)
def test_per_user_not_active_before_phase():
df = calculate_per_user_ai_cost(
CTM_DEFAULT_SITES, _scope("AI Translate", phase=3),
DEFAULT_METERS["AI Translate"], DEFAULT_PRICING, year=2,
def test_ai_translate_not_active_before_phase():
"""AI Translate (consumption meter) produces zero cost before its phase."""
scenario = get_scenario("realistic")
apac_sites = [s.site_name for s in CTM_DEFAULT_SITES if s.region_pricing == "APAC"]
df = calculate_consumption_ai_cost(
CTM_DEFAULT_SITES,
_scope("AI Translate", apac_sites, phase=3),
DEFAULT_METERS["AI Translate"], scenario, DEFAULT_PRICING, year=2,
)
assert df["annual_cost"].sum() == 0
@@ -63,7 +67,7 @@ def test_copilot_covers_supervisor_summary():
total = calculate_total_cost(
CTM_DEFAULT_SITES,
[
_scope("Agent Copilot"),
_scope("Agent Copilot [named]"),
_scope("AI Summary & Insights"),
],
DEFAULT_METERS, DEFAULT_PRICING, scenario, year=1,
@@ -111,8 +115,8 @@ def test_regional_pricing_not_hardcoded():
pricing["APAC"] = TokenPricing(region="APAC", list_rate_per_token=2.0)
apac_site = next(s for s in CTM_DEFAULT_SITES if s.region_pricing == "APAC")
df = calculate_per_user_ai_cost(
[apac_site], _scope("Speech & Text Analytics", [apac_site.site_name]),
DEFAULT_METERS["Speech & Text Analytics"], pricing,
[apac_site], _scope("Speech & Text Analytics [named]", [apac_site.site_name]),
DEFAULT_METERS["Speech & Text Analytics [named]"], pricing,
)
expected = apac_site.named_users * 30 * 12 * 2.0
assert df["annual_cost"].sum() == pytest.approx(expected)

View File

@@ -10,10 +10,26 @@ from tokencalc.meters import Confidence, MeterType, TokenMeter, TokenPricing
def test_all_spec_meters_present():
expected = {
"Voice Bot", "Virtual Agent (legacy)", "Agentic Virtual Agent",
"AI Summary & Insights", "Direct Messaging", "Social Listening",
"Social Responses", "Speech & Text Analytics", "Agent Copilot",
"Email AI (Auto-Suggest)", "Email AI (Auto-Respond)", "AI Translate",
# Voice / Bot
"Voice Bot", "Digital Bot",
# Virtual Agent
"Virtual Agent (legacy)", "Agentic Virtual Agent",
# Agent Copilot (named + concurrent)
"Agent Copilot [named]", "Agent Copilot [concurrent]",
# AI Quality / Analytics
"AI Scoring", "AI Summary & Insights",
# Speech & Text Analytics (named + concurrent)
"Speech & Text Analytics [named]", "Speech & Text Analytics [concurrent]",
# Routing
"Predictive Routing",
# Messaging
"Direct Messaging", "Social Listening", "Social Responses",
# Language
"AI Translate",
# Genesys Cloud Copilot
"Genesys Cloud Copilot",
# Email AI (rates TBD)
"Email AI (Auto-Suggest)", "Email AI (Auto-Respond)",
}
assert expected == set(DEFAULT_METERS)
@@ -22,17 +38,29 @@ def test_confirmed_rates():
m = DEFAULT_METERS
assert m["Voice Bot"].units_per_token == 17
assert m["Voice Bot"].tokens_per_unit == pytest.approx(0.0588, abs=1e-3)
assert m["Digital Bot"].units_per_token == 51
assert m["Agentic Virtual Agent"].tokens_per_unit == 1.2
assert m["AI Summary & Insights"].tokens_per_unit == 0.02
assert m["Direct Messaging"].units_per_token == 400
assert m["Speech & Text Analytics"].tokens_per_unit == 30
assert m["Agent Copilot"].tokens_per_unit == 40
# Named variants
assert m["Speech & Text Analytics [named]"].tokens_per_unit == 30
assert m["Speech & Text Analytics [concurrent]"].tokens_per_unit == 45
assert m["Agent Copilot [named]"].tokens_per_unit == 40
assert m["Agent Copilot [concurrent]"].tokens_per_unit == 60
# AI Translate is now a confirmed consumption meter
assert m["AI Translate"].tokens_per_unit == 0.5
assert m["AI Translate"].units_per_token == 2
assert m["AI Translate"].confidence is Confidence.CONFIRMED
# New meters
assert m["AI Scoring"].units_per_token == 20
assert m["Predictive Routing"].units_per_token == 17
assert m["Genesys Cloud Copilot"].units_per_token == 20
def test_unknown_meters_flagged():
unknown = {f for f, m in DEFAULT_METERS.items() if m.confidence is Confidence.UNKNOWN}
assert unknown == {
"Email AI (Auto-Suggest)", "Email AI (Auto-Respond)", "AI Translate"
"Email AI (Auto-Suggest)", "Email AI (Auto-Respond)",
}
assert Confidence.UNKNOWN.icon == "🔴"
assert Confidence.CONFIRMED.icon == "🟢"

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},
),
}