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_acw_summarization_benefit,
calculate_email_ai_benefit, calculate_email_ai_benefit,
calculate_total_benefit, calculate_total_benefit,
calculate_va_deflection_benefit,
) )
from tokencalc.defaults import CTM_DEFAULT_FEATURE_SCOPES, CTM_DEFAULT_SITES from tokencalc.defaults import CTM_DEFAULT_FEATURE_SCOPES, CTM_DEFAULT_SITES
from tokencalc.inputs import WORKING_SECONDS_PER_YEAR, FeatureScope, SiteInput 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] 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(): def test_working_seconds_constant():
assert WORKING_SECONDS_PER_YEAR == 2_080 * 3_600 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( scenario_state_to_json(
CTM_DEFAULT_SITES, CTM_DEFAULT_TAKEOUTS, CTM_DEFAULT_FEATURE_SCOPES, p 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 [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 takeouts[0].annual_cost == CTM_DEFAULT_TAKEOUTS[0].annual_cost
assert scopes[0].adoption_curve == CTM_DEFAULT_FEATURE_SCOPES[0].adoption_curve 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(): def test_sta_acceptance_number():
"""2,088 users × 30 tokens × 12 months × $1 = $751,680.""" """2,088 users × 30 tokens × 12 months × $1 = $751,680."""
df = calculate_per_user_ai_cost( df = calculate_per_user_ai_cost(
CTM_DEFAULT_SITES, _scope("Speech & Text Analytics"), CTM_DEFAULT_SITES, _scope("Speech & Text Analytics [named]"),
DEFAULT_METERS["Speech & Text Analytics"], DEFAULT_PRICING, DEFAULT_METERS["Speech & Text Analytics [named]"], DEFAULT_PRICING,
) )
assert df["annual_cost"].sum() == pytest.approx(751_680) assert df["annual_cost"].sum() == pytest.approx(751_680)
@@ -43,16 +43,20 @@ def test_sta_acceptance_number():
def test_agent_copilot_acceptance_number(): def test_agent_copilot_acceptance_number():
"""2,088 users × 40 tokens × 12 months × $1 = $1,002,240.""" """2,088 users × 40 tokens × 12 months × $1 = $1,002,240."""
df = calculate_per_user_ai_cost( df = calculate_per_user_ai_cost(
CTM_DEFAULT_SITES, _scope("Agent Copilot"), CTM_DEFAULT_SITES, _scope("Agent Copilot [named]"),
DEFAULT_METERS["Agent Copilot"], DEFAULT_PRICING, DEFAULT_METERS["Agent Copilot [named]"], DEFAULT_PRICING,
) )
assert df["annual_cost"].sum() == pytest.approx(1_002_240) assert df["annual_cost"].sum() == pytest.approx(1_002_240)
def test_per_user_not_active_before_phase(): def test_ai_translate_not_active_before_phase():
df = calculate_per_user_ai_cost( """AI Translate (consumption meter) produces zero cost before its phase."""
CTM_DEFAULT_SITES, _scope("AI Translate", phase=3), scenario = get_scenario("realistic")
DEFAULT_METERS["AI Translate"], DEFAULT_PRICING, year=2, 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 assert df["annual_cost"].sum() == 0
@@ -63,7 +67,7 @@ def test_copilot_covers_supervisor_summary():
total = calculate_total_cost( total = calculate_total_cost(
CTM_DEFAULT_SITES, CTM_DEFAULT_SITES,
[ [
_scope("Agent Copilot"), _scope("Agent Copilot [named]"),
_scope("AI Summary & Insights"), _scope("AI Summary & Insights"),
], ],
DEFAULT_METERS, DEFAULT_PRICING, scenario, year=1, 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) 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") apac_site = next(s for s in CTM_DEFAULT_SITES if s.region_pricing == "APAC")
df = calculate_per_user_ai_cost( df = calculate_per_user_ai_cost(
[apac_site], _scope("Speech & Text Analytics", [apac_site.site_name]), [apac_site], _scope("Speech & Text Analytics [named]", [apac_site.site_name]),
DEFAULT_METERS["Speech & Text Analytics"], pricing, DEFAULT_METERS["Speech & Text Analytics [named]"], pricing,
) )
expected = apac_site.named_users * 30 * 12 * 2.0 expected = apac_site.named_users * 30 * 12 * 2.0
assert df["annual_cost"].sum() == pytest.approx(expected) 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(): def test_all_spec_meters_present():
expected = { expected = {
"Voice Bot", "Virtual Agent (legacy)", "Agentic Virtual Agent", # Voice / Bot
"AI Summary & Insights", "Direct Messaging", "Social Listening", "Voice Bot", "Digital Bot",
"Social Responses", "Speech & Text Analytics", "Agent Copilot", # Virtual Agent
"Email AI (Auto-Suggest)", "Email AI (Auto-Respond)", "AI Translate", "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) assert expected == set(DEFAULT_METERS)
@@ -22,17 +38,29 @@ def test_confirmed_rates():
m = DEFAULT_METERS m = DEFAULT_METERS
assert m["Voice Bot"].units_per_token == 17 assert m["Voice Bot"].units_per_token == 17
assert m["Voice Bot"].tokens_per_unit == pytest.approx(0.0588, abs=1e-3) 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["Agentic Virtual Agent"].tokens_per_unit == 1.2
assert m["AI Summary & Insights"].tokens_per_unit == 0.02 assert m["AI Summary & Insights"].tokens_per_unit == 0.02
assert m["Direct Messaging"].units_per_token == 400 assert m["Direct Messaging"].units_per_token == 400
assert m["Speech & Text Analytics"].tokens_per_unit == 30 # Named variants
assert m["Agent Copilot"].tokens_per_unit == 40 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(): def test_unknown_meters_flagged():
unknown = {f for f, m in DEFAULT_METERS.items() if m.confidence is Confidence.UNKNOWN} unknown = {f for f, m in DEFAULT_METERS.items() if m.confidence is Confidence.UNKNOWN}
assert 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.UNKNOWN.icon == "🔴"
assert Confidence.CONFIRMED.icon == "🟢" assert Confidence.CONFIRMED.icon == "🟢"

View File

@@ -206,7 +206,7 @@ def calculate_sta_benefit(
return _df(rows) return _df(rows)
def calculate_bot_deflection_benefit( def calculate_va_deflection_benefit(
sites: list[SiteInput], sites: list[SiteInput],
feature_scope: FeatureScope, feature_scope: FeatureScope,
scenario: str | Scenario, scenario: str | Scenario,
@@ -214,41 +214,85 @@ def calculate_bot_deflection_benefit(
params: str = "realistic", params: str = "realistic",
rollout: RolloutPlan | None = None, rollout: RolloutPlan | None = None,
) -> pd.DataFrame: ) -> 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 **Layered (sequential) deflection model** — Voice Bot runs first on
case — deflected volume never reaches an agent, so the full AHT is the full call pool; Agentic VA handles a share of the *residual*
avoided. (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 sc = get_scenario(scenario) if isinstance(scenario, str) else scenario
ro = rollout or NO_ROLLOUT ro = rollout or NO_ROLLOUT
realization = sc.realization(year) 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 = [] rows = []
for s in sites: for s in sites:
if not feature_scope.active(s.site_name, year): if not feature_scope.active(s.site_name, year):
continue continue
if feature_scope.feature == "Voice Bot": if feature_scope.feature == "Voice Bot":
deflection = ( # Bot operates on the full call pool.
bot_rate = (
feature_scope.deflection_target feature_scope.deflection_target
if feature_scope.deflection_target is not None if feature_scope.deflection_target is not None
else sc.voice_bot_deflection else sc.voice_bot_deflection
) )
deflected_calls = s.voice_volume_monthly * MONTHS_PER_YEAR * bot_rate
else: # Agentic Virtual Agent 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 feature_scope.deflection_target
if feature_scope.deflection_target is not None if feature_scope.deflection_target is not None
else sc.agentic_va_deflection else sc.agentic_va_deflection
) )
seconds_saved = ( residual_calls = (
s.voice_volume_monthly * MONTHS_PER_YEAR s.voice_volume_monthly * MONTHS_PER_YEAR * (1.0 - bot_rate)
* deflection * s.voice_aht_seconds * realization )
) deflected_calls = residual_calls * va_rate
seconds_saved = deflected_calls * s.voice_aht_seconds * realization
rows.append( rows.append(
{ {
"benefit_line": f"{feature_scope.feature} deflection (labour avoided)", "benefit_line": f"{feature_scope.feature} deflection (labour avoided)",
"scope": s.site_name, "scope": s.site_name,
"annual_value": seconds_saved * s.agent_cost_per_second "annual_value": (
* ro.fraction_live(s.site_name, year), seconds_saved
* s.agent_cost_per_second
* realization_factor
* ro.fraction_live(s.site_name, year)
),
"confidence": Confidence.ESTIMATED.value, "confidence": Confidence.ESTIMATED.value,
} }
) )
@@ -320,19 +364,30 @@ def calculate_predictive_routing_benefit(
#: Which calculator handles which feature scope. #: 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 = { _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_voice_handle_time_benefit,
calculate_acw_summarization_benefit, calculate_acw_summarization_benefit,
), ),
"AI Summary & Insights": (), # benefit carried by Copilot where present "AI Summary & Insights": (), # benefit carried by Copilot where present
"Speech & Text Analytics": (calculate_sta_benefit,), "Speech & Text Analytics [named]": (calculate_sta_benefit,),
"Voice Bot": (calculate_bot_deflection_benefit,), "Speech & Text Analytics [concurrent]": (calculate_sta_benefit,),
"Agentic Virtual Agent": (calculate_bot_deflection_benefit,), "Voice Bot": (calculate_va_deflection_benefit,),
"Email AI (Auto-Respond)": (calculate_email_ai_benefit,), "Agentic Virtual Agent": (calculate_va_deflection_benefit,),
"Predictive Routing": (calculate_predictive_routing_benefit,), "Predictive Routing": (calculate_predictive_routing_benefit,),
} }
_COPILOT_FEATURES = {"Agent Copilot [named]", "Agent Copilot [concurrent]"}
def calculate_total_benefit( def calculate_total_benefit(
sites: list[SiteInput], sites: list[SiteInput],
@@ -346,10 +401,18 @@ def calculate_total_benefit(
"""All benefit lines for one scenario-year, aggregated per line. """All benefit lines for one scenario-year, aggregated per line.
Returns DataFrame: benefit_line, scope, annual_value, confidence. 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 sc = get_scenario(scenario) if isinstance(scenario, str) else scenario
frames: list[pd.DataFrame] = [] 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 scope in feature_scopes:
for fn in _BENEFIT_DISPATCH.get(scope.feature, ()): # type: ignore[arg-type] 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 enabled at a site, AI Summary & Insights consumption at that site
is forced to zero — Copilot's per-user token rate already includes is forced to zero — Copilot's per-user token rate already includes
interaction summarization. Source: Genesys Cloud AI Experience interaction summarization. Source: Genesys Cloud AI Experience
tokens FAQ, token metering,
https://help.mypurecloud.com/articles/genesys-cloud-ai-experience-tokens-faqs/ https://help.genesys.cloud/articles/genesys-cloud-tokens-model/
2. **Token rounding.** Genesys rounds consumption up at billing — 2. **Token rounding.** Genesys rounds consumption up at billing —
``math.ceil`` is applied to each site's MONTHLY consumption token ``math.ceil`` is applied to each site's MONTHLY consumption token
total before the rate. Per-user totals (users × tokens/user/month) 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 site.voice_volume_monthly * deflection * scenario.voice_bot_avg_minutes
) # minutes ) # minutes
if feature == "Agentic Virtual Agent": 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 scope.deflection_target
if scope.deflection_target is not None if scope.deflection_target is not None
else scenario.agentic_va_deflection 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)": if feature == "Virtual Agent (legacy)":
deflection = scope.deflection_target or 0.0 deflection = scope.deflection_target or 0.0
return site.voice_volume_monthly * deflection 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"): if feature in ("Direct Messaging", "Social Listening", "Social Responses"):
eligibility = scope.eligibility_pct if scope.eligibility_pct is not None else 1.0 eligibility = scope.eligibility_pct if scope.eligibility_pct is not None else 1.0
return (site.chat_volume_monthly + site.sms_volume_monthly) * eligibility 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}") 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 # Rule 1: Agent Copilot covers Supervisor AI Summary. Sites where
# Copilot is active this year are excluded from AI Summary billing — # Copilot is active this year are excluded from AI Summary billing —
# Copilot's 40 tokens/user/month already includes summarization. # Copilot's per-user token rate already includes interaction summarization.
# https://help.mypurecloud.com/articles/genesys-cloud-ai-experience-tokens-faqs/ # https://help.genesys.cloud/articles/genesys-cloud-tokens-model/
_COPILOT_FEATURES = {"Agent Copilot [named]", "Agent Copilot [concurrent]"}
copilot_sites: set[str] = set() copilot_sites: set[str] = set()
for scope in feature_scopes: for scope in feature_scopes:
if scope.feature == "Agent Copilot": if scope.feature in _COPILOT_FEATURES:
copilot_sites |= { copilot_sites |= {
s.site_name for s in sites if scope.active(s.site_name, year) 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. #: analysis horizon in the P&L. ESTIMATED — confirm with delivery team.
DEFAULT_IMPLEMENTATION_COST = 0.0 DEFAULT_IMPLEMENTATION_COST = 0.0
_GENESYS_TOKEN_FAQ = ( _GENESYS_TOKEN_METERS = (
"https://help.mypurecloud.com/articles/genesys-cloud-ai-experience-tokens-faqs/" "https://help.genesys.cloud/articles/genesys-cloud-tokens-model/"
) )
# ── Token meters ───────────────────────────────────────────────────── # ── Token meters ─────────────────────────────────────────────────────
@@ -41,6 +41,7 @@ _GENESYS_TOKEN_FAQ = (
DEFAULT_METERS: dict[str, TokenMeter] = { DEFAULT_METERS: dict[str, TokenMeter] = {
m.feature: m m.feature: m
for m in [ for m in [
# ── Voice / Bot ───────────────────────────────────────────────
TokenMeter( TokenMeter(
feature="Voice Bot", feature="Voice Bot",
meter_type=MeterType.PER_MINUTE, meter_type=MeterType.PER_MINUTE,
@@ -48,16 +49,26 @@ DEFAULT_METERS: dict[str, TokenMeter] = {
tokens_per_unit=1 / 17, # 0.0588 tokens_per_unit=1 / 17, # 0.0588
confidence=Confidence.CONFIRMED, confidence=Confidence.CONFIRMED,
notes="IVR self-service voice bot minutes; 17 min per token.", 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( TokenMeter(
feature="Virtual Agent (legacy)", feature="Virtual Agent (legacy)",
meter_type=MeterType.PER_INTERACTION, meter_type=MeterType.PER_INTERACTION,
units_per_token=2.0, units_per_token=2.0,
tokens_per_unit=0.5, tokens_per_unit=0.5,
confidence=Confidence.CONFIRMED, confidence=Confidence.CONFIRMED,
notes="Legacy (non-agentic) virtual agent; 2 interactions per token.", notes="Legacy (non-agentic) virtual agent; 0.5 tokens per interaction.",
source_url=_GENESYS_TOKEN_FAQ, source_url=_GENESYS_TOKEN_METERS,
), ),
TokenMeter( TokenMeter(
feature="Agentic Virtual Agent", feature="Agentic Virtual Agent",
@@ -66,7 +77,42 @@ DEFAULT_METERS: dict[str, TokenMeter] = {
tokens_per_unit=1.2, tokens_per_unit=1.2,
confidence=Confidence.CONFIRMED, confidence=Confidence.CONFIRMED,
notes="Agentic VA; 1.2 tokens per interaction.", 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( TokenMeter(
feature="AI Summary & Insights", feature="AI Summary & Insights",
@@ -78,16 +124,50 @@ DEFAULT_METERS: dict[str, TokenMeter] = {
"Supervisor standalone summarization; 50 summaries per token. " "Supervisor standalone summarization; 50 summaries per token. "
"NOT metered where Agent Copilot is assigned — see cost model." "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( TokenMeter(
feature="Direct Messaging", feature="Direct Messaging",
meter_type=MeterType.PER_MESSAGE, meter_type=MeterType.PER_MESSAGE,
units_per_token=400.0, units_per_token=400.0,
tokens_per_unit=0.0025, tokens_per_unit=0.0025,
confidence=Confidence.CONFIRMED, confidence=Confidence.CONFIRMED,
notes="FB/IG/WhatsApp messages; 400 messages per token.", notes=(
source_url=_GENESYS_TOKEN_FAQ, "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( TokenMeter(
feature="Social Listening", feature="Social Listening",
@@ -95,8 +175,8 @@ DEFAULT_METERS: dict[str, TokenMeter] = {
units_per_token=400.0, units_per_token=400.0,
tokens_per_unit=0.0025, tokens_per_unit=0.0025,
confidence=Confidence.CONFIRMED, confidence=Confidence.CONFIRMED,
notes="400 messages per token.", notes="Genesys Cloud Social; 400 social post ingestions per channel per token.",
source_url=_GENESYS_TOKEN_FAQ, source_url=_GENESYS_TOKEN_METERS,
), ),
TokenMeter( TokenMeter(
feature="Social Responses", feature="Social Responses",
@@ -104,53 +184,48 @@ DEFAULT_METERS: dict[str, TokenMeter] = {
units_per_token=400.0, units_per_token=400.0,
tokens_per_unit=0.0025, tokens_per_unit=0.0025,
confidence=Confidence.CONFIRMED, confidence=Confidence.CONFIRMED,
notes="400 messages per token.", notes="Social Post Responses; 400 outbound messages per channel per token.",
source_url=_GENESYS_TOKEN_FAQ, source_url=_GENESYS_TOKEN_METERS,
), ),
# ── Language / Translation ────────────────────────────────────
TokenMeter( TokenMeter(
feature="Speech & Text Analytics", feature="AI Translate",
meter_type=MeterType.PER_USER_PER_MONTH, meter_type=MeterType.PER_INTERACTION,
units_per_token=0.0, # n/a for per-user meters units_per_token=2.0,
tokens_per_unit=30.0, tokens_per_unit=0.5,
confidence=Confidence.CONFIRMED, confidence=Confidence.CONFIRMED,
notes="STA: 30 tokens per named user per month.", notes="AI translation; 2 translations per token.",
source_url=_GENESYS_TOKEN_FAQ, source_url=_GENESYS_TOKEN_METERS,
), ),
# ── Genesys Cloud Copilot ─────────────────────────────────────
TokenMeter( TokenMeter(
feature="Agent Copilot", feature="Genesys Cloud Copilot",
meter_type=MeterType.PER_USER_PER_MONTH, meter_type=MeterType.PER_INTERACTION,
units_per_token=0.0, units_per_token=20.0,
tokens_per_unit=40.0, tokens_per_unit=0.05,
confidence=Confidence.CONFIRMED, confidence=Confidence.CONFIRMED,
notes=( notes=(
"40 tokens per named user per month. Includes interaction " "20 AI actions per token; Genesys Cloud knowledge queries "
"summarization (covers AI Summary & Insights)." "are not charged."
), ),
source_url=_GENESYS_TOKEN_FAQ, source_url=_GENESYS_TOKEN_METERS,
), ),
# ── Email AI (rates not yet published) ────────────────────────
TokenMeter( TokenMeter(
feature="Email AI (Auto-Suggest)", feature="Email AI (Auto-Suggest)",
meter_type=MeterType.PER_USER_PER_MONTH, meter_type=MeterType.PER_USER_PER_MONTH,
units_per_token=0.0, units_per_token=0.0,
tokens_per_unit=30.0, # TBD — working default tokens_per_unit=0.0,
confidence=Confidence.UNKNOWN, confidence=Confidence.UNKNOWN,
notes="Rate not yet sourced. Working default 30 tokens/user/month.", notes="Requires Agent Copilot. Token rate not yet published.",
), ),
TokenMeter( TokenMeter(
feature="Email AI (Auto-Respond)", feature="Email AI (Auto-Respond)",
meter_type=MeterType.PER_MESSAGE, 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, units_per_token=0.0,
tokens_per_unit=20.0, # TBD — working default tokens_per_unit=0.0,
confidence=Confidence.UNKNOWN, 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("Voice Bot", ALL_SITE_NAMES, phase=1, adoption_curve=_RAMP),
FeatureScope("Agentic Virtual Agent", ["NAM", "EMEA"], phase=2, FeatureScope("Agentic Virtual Agent", ["NAM", "EMEA"], phase=2,
adoption_curve={2: 0.70, 3: 1.0}), adoption_curve={2: 0.70, 3: 1.0}),
FeatureScope("Speech & Text Analytics", ALL_SITE_NAMES, phase=1), # CTM has named licences — use the [named] variant for both STA and Copilot.
FeatureScope("Agent Copilot", ALL_SITE_NAMES, phase=1), 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, FeatureScope("AI Summary & Insights", ALL_SITE_NAMES, phase=1,
adoption_curve=_RAMP), adoption_curve=_RAMP),
FeatureScope("Direct Messaging", 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-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", FeatureScope("AI Translate",
["APAC HK", "APAC SG", "APAC SH", "APAC GZ", "APAC JP", "APAC TW"], ["APAC HK", "APAC SG", "APAC SH", "APAC GZ", "APAC JP", "APAC TW"],
phase=3), phase=3),

View File

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

View File

@@ -18,12 +18,28 @@ class Scenario:
# ── Cost-side drivers ─────────────────────────────────────────── # ── Cost-side drivers ───────────────────────────────────────────
voice_bot_deflection: float # share of voice volume deflected to bot voice_bot_deflection: float # share of voice volume deflected to bot
voice_bot_avg_minutes: float # bot minutes per deflected call 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_summarization_eligibility: float
voice_knowledge_eligibility: float voice_knowledge_eligibility: float
email_auto_respond_rate: float # share of email auto-responded email_auto_respond_rate: float # share of email auto-responded
email_auto_suggest_acceptance: float 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 # year -> fraction of full benefit realized
benefit_realization: dict[int, float] = field(default_factory=dict) 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_aht_reduction": {"claim": 0.18, "realistic": 0.085}, # 5-12% Y1
"digital_acw_reduction": {"claim": 1.00, "realistic": 0.40}, # 30-50% 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 "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): # ESTIMATED lines (no Genesys claim published):
"supervisor_copilot_time_saving": {"claim": 0.10, "realistic": 0.05}, "supervisor_copilot_time_saving": {"claim": 0.10, "realistic": 0.05},
"predictive_routing_aht_reduction": {"claim": 0.04, "realistic": 0.02}, "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, voice_knowledge_eligibility=0.40,
email_auto_respond_rate=0.10, email_auto_respond_rate=0.10,
email_auto_suggest_acceptance=0.25, 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}, benefit_realization={1: 0.30, 2: 0.60, 3: 0.80},
), ),
"realistic": Scenario( "realistic": Scenario(
@@ -87,6 +114,11 @@ SCENARIOS: dict[str, Scenario] = {
voice_knowledge_eligibility=0.60, voice_knowledge_eligibility=0.60,
email_auto_respond_rate=0.20, email_auto_respond_rate=0.20,
email_auto_suggest_acceptance=0.40, 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}, benefit_realization={1: 0.50, 2: 0.80, 3: 0.95},
), ),
"stretch": Scenario( "stretch": Scenario(
@@ -98,6 +130,11 @@ SCENARIOS: dict[str, Scenario] = {
voice_knowledge_eligibility=0.80, voice_knowledge_eligibility=0.80,
email_auto_respond_rate=0.50, email_auto_respond_rate=0.50,
email_auto_suggest_acceptance=0.60, 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}, benefit_realization={1: 0.75, 2: 0.95, 3: 1.00},
), ),
} }

View File

@@ -0,0 +1,56 @@
# Genesys Cloud AI Experience metering details
2026-06-07
https://help.genesys.cloud/articles/genesys-cloud-tokens-model/
This table describes the Genesys Cloud AI products raw meter and how many tokens are consumed.
Note: Direct Messaging (DM) channels include inbound and outbound messages. Social Responses messages include outbound messages only.
Genesys Cloud product Units per token
Bots (Voice) 17 minutes per token. For more information, see the AI section of the Genesys Cloud pricing hub.
Bots (Digital) 51 sessions per token. For more information, see the AI section of the Genesys Cloud pricing hub.
Virtual Agent 0.5 tokens per Virtual Agent interaction - An interaction is defined in accordance with existing Genesys Cloud token-based pricing.
Agentic Virtual Agent 1.2 tokens per interaction - An interaction is defined in accordance with existing Genesys Cloud token-based pricing.
Agent Copilot [concurrent] One user requires 60 tokens
Agent Copilot [named] One user requires 40 tokens
AI Scoring 20 evaluations scored with AI scoring per token
AI Translate Two translations per token
AI Summary and Insights
50 AI summaries/insights per token. However, if you enable Agent Copilot simultaneously, then Supervisor Copilot summaries and insights do not consume tokens and instead rely on Agent Copilot functionality.
Apple Messages for Business 400 inbound or outbound messages per token.
Facebook Messenger† 400 inbound or outbound messages per token.
Instagram Direct Messaging† 400 inbound or outbound messages per token.
WhatsApp Messaging† 400 inbound or outbound messages per token.
Other charges apply for WhatsApp. For more information, see the Messaging section of the Genesys Cloud pricing hub.
X (formerly Twitter) Direct Messaging 400 inbound or outbound messages per token.
Other charges apply for X integrations. For more information, see the Messaging section of the Genesys Cloud pricing hub.
Genesys Cloud Social 400 social post ingestions per channel per token. For more information, see the Messaging section of the Genesys Cloud pricing hub.
Social Post Responses 400 outbound messages per channel per token.
Predictive Engagement No charge for token usage. For more information, see Can I use digital user tracking at no additional cost? and Predictive Engagement and digital user tracking.
Predictive routing 17 routes per token. One token is consumed for every 17 interactions routed with predictive routing. For more information, see Predictive routing overview.
Speech and Text Analytics [named] One user requires 30 tokens
Speech and Text Analytics [concurrent] One user requires 45 tokens
Genesys Cloud Copilot 20 AI actions per token, no charge for Genesys Cloud knowledge queries. For more information, see Genesys Cloud Copilot AI actions overview.
† For Facebook, Instagram, and WhatsApp: If the organization has not moved to the Genesys Cloud AI Experience token pricing, then legacy, conversation-based pricing applies. Other charges apply for X integrations. For more information, see Messaging in the Genesys Cloud pricing hub.
## Virtual Agent interactions explained
A single interaction is contained by a single billingID. A billingID represents a single interaction on any channel of any length. A single interaction is delimited by end interaction events. A single billingID can contain multiple end interaction events.
The following actions trigger end interaction events:
Exit action in flow (return calling flow)
Disconnect action in the flow
Disconnect when the participant hangs up the phone
Disconnect via a Transfer to ACD action
Exit or disconnect handling for an unexpected error
Exit or disconnect handing for a recognition failure (continuous no matches or no inputs)
Exit or disconnect handling for Max No Input override (if set, overrides recognition failure settings)
Exit handling for agent escalation
Digital expiry after inactivity (72-hour async timeout)
When Genesys Cloud transfers interactions between inbound flows and Virtual Agent flows, the same billingID remains. When an action triggers an end interaction event and transfers no longer occur, then Genesys Cloud closes the billingID.

View File

@@ -32,7 +32,7 @@
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"✅ Athena connected — https://athena.ouranos.helu.ca (1 report templates visible)\n", "✅ Athena connected — https://athena.ouranos.helu.ca (2 report templates visible)\n",
"📁 Study: 202512_GenesysCX\n" "📁 Study: 202512_GenesysCX\n"
] ]
} }
@@ -69,7 +69,7 @@
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"Created report template UCb2hSJprSBx\n" "Found existing report template UCb2hSJprSBx (status: active)\n"
] ]
} }
], ],
@@ -114,7 +114,7 @@
"`(1 + risk_adj)`; Year-0 amounts use companion `*_initial` fields.\n", "`(1 + risk_adj)`; Year-0 amounts use companion `*_initial` fields.\n",
"The `genesys_ai_tokens` line is seeded \\$0 (reproduces the published study) —\n", "The `genesys_ai_tokens` line is seeded \\$0 (reproduces the published study) —\n",
"the annual cost gets entered per deal, from the Genesys quote, in\n", "the annual cost gets entered per deal, from the Genesys quote, in\n",
"`01_business_case.ipynb`." "`03_business_case.ipynb`."
] ]
}, },
{ {
@@ -127,8 +127,7 @@
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"12 fields created, 0 already existed.\n", "0 fields created, 12 already existed.\n"
"Report template activated.\n"
] ]
} }
], ],
@@ -305,7 +304,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 6, "execution_count": 5,
"id": "1e375b54", "id": "1e375b54",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@@ -445,7 +444,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 7, "execution_count": 6,
"id": "584e01dd", "id": "584e01dd",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@@ -531,7 +530,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 8, "execution_count": 7,
"id": "e04b1676", "id": "e04b1676",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@@ -539,7 +538,6 @@
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"Auto-selected proposal 1: Secure Cloud Infrastructure Modernization\n",
"Attaching via: {'proposal': 1}\n" "Attaching via: {'proposal': 1}\n"
] ]
} }
@@ -589,7 +587,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 9, "execution_count": 8,
"id": "0655d1fc", "id": "0655d1fc",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@@ -597,7 +595,7 @@
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"Created tool 3rzDgVdsjhVv attached to {'proposal': 1}\n" "Found existing tool 3rzDgVdsjhVv (status: draft)\n"
] ]
} }
], ],
@@ -643,7 +641,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 10, "execution_count": 9,
"id": "86443d76", "id": "86443d76",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@@ -696,7 +694,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 11, "execution_count": 10,
"id": "0728b42e", "id": "0728b42e",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@@ -724,7 +722,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 12, "execution_count": 11,
"id": "aba8fc21", "id": "aba8fc21",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@@ -853,7 +851,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 13, "execution_count": 12,
"id": "d8102590", "id": "d8102590",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@@ -861,13 +859,12 @@
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"Saved version 1 (baseline).\n",
"Saved to /Users/robert/git/palladium/.env:\n", "Saved to /Users/robert/git/palladium/.env:\n",
" PALLADIUM_GENESYSCX_REPORT_PUBLIC_ID=UCb2hSJprSBx\n", " PALLADIUM_GENESYSCX_REPORT_PUBLIC_ID=UCb2hSJprSBx\n",
" PALLADIUM_GENESYSCX_TOOL_PUBLIC_ID=3rzDgVdsjhVv\n", " PALLADIUM_GENESYSCX_TOOL_PUBLIC_ID=3rzDgVdsjhVv\n",
" PALLADIUM_GENESYSCX_PROPOSAL_ID=1\n", " PALLADIUM_GENESYSCX_PROPOSAL_ID=1\n",
"\n", "\n",
"Next → 01_business_case.ipynb (AI token cost entry + sensitivity).\n" "Next → 01_benefits.ipynb (walk through the four Forrester benefits).\n"
] ]
} }
], ],
@@ -876,7 +873,7 @@
" client.save_version(TOOL_ID, note=(\n", " client.save_version(TOOL_ID, note=(\n",
" \"Baseline — published Forrester CX Cloud TEI figures (Dec 2025). \"\n", " \"Baseline — published Forrester CX Cloud TEI figures (Dec 2025). \"\n",
" \"genesys_ai_tokens at $0 per the published study; set the annual \"\n", " \"genesys_ai_tokens at $0 per the published study; set the annual \"\n",
" \"cost from the Genesys quote in 01_business_case before client use.\"\n", " \"cost from the Genesys quote in 03_business_case before client use.\"\n",
" ))\n", " ))\n",
" print(\"Saved version 1 (baseline).\")\n", " print(\"Saved version 1 (baseline).\")\n",
"\n", "\n",
@@ -893,7 +890,7 @@
"print(f\"Saved to {env_path}:\")\n", "print(f\"Saved to {env_path}:\")\n",
"for k, v in ids.items():\n", "for k, v in ids.items():\n",
" print(f\" {k}={v}\")\n", " print(f\" {k}={v}\")\n",
"print(\"\\nNext → 01_business_case.ipynb (AI token cost entry + sensitivity).\")" "print(\"\\nNext → 01_benefits.ipynb (walk through the four Forrester benefits).\")"
] ]
}, },
{ {
@@ -903,6 +900,14 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [] "source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "13acdc34-71f6-4220-8675-4e1527cb8e39",
"metadata": {},
"outputs": [],
"source": []
} }
], ],
"metadata": { "metadata": {

File diff suppressed because one or more lines are too long

View File

@@ -1,376 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "3da9de99",
"metadata": {},
"source": [
"# 01 · Business Case — Genesys CX Cloud TEI\n",
"\n",
"Working view of the live Athena tool, plus the **Genesys AI Experience token**\n",
"cost line the published study omits. Run `00_provision.ipynb` first.\n",
"\n",
"The published study models \\$0 AI consumption while three of its four benefits\n",
"(self-service uplift, agent efficiency, agent-assist sales) depend on AI\n",
"capabilities that Genesys bills via AI Experience tokens. Token pricing is\n",
"tiered and deal-specific, so the model keeps it simple — **one annual cost\n",
"value, entered from the Genesys quote**, exactly as Athena stores it. Quote\n",
"details (volume, unit price, tier) go in the field notes for the audit trail."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "89deae70",
"metadata": {},
"outputs": [],
"source": [
"import sys, pathlib # path shim: works on a fresh kernel\n",
"for _p in [pathlib.Path.cwd(), *pathlib.Path.cwd().parents]:\n",
" if (_p / \"pyproject.toml\").exists():\n",
" sys.path.insert(0, str(_p)); break\n",
"\n",
"import pandas as pd\n",
"from core.bootstrap import init\n",
"from core.notebook_helpers import charts\n",
"\n",
"pal = init(study=\"202512_GenesysCX\")\n",
"client, seed, config = pal.client, pal.seed_data, pal.config\n",
"\n",
"TOOL_ID = config.TOOL_PUBLIC_ID\n",
"assert TOOL_ID, \"No PALLADIUM_GENESYSCX_TOOL_PUBLIC_ID in .env — run 00_provision.ipynb first.\"\n",
"tool = client.get_tool(TOOL_ID)\n",
"print(f\"Tool: {tool.get('name')} ({TOOL_ID}) status={tool.get('status')}\")"
]
},
{
"cell_type": "markdown",
"id": "09afaf70",
"metadata": {},
"source": [
"## Current financial summary"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3d80c19a",
"metadata": {},
"outputs": [],
"source": [
"summary = client.calculate(TOOL_ID)\n",
"client.print_summary(TOOL_ID)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "392df0ba",
"metadata": {},
"outputs": [],
"source": [
"values = client.get_values(TOOL_ID)\n",
"benefit_rows = [v for v in values if v.get(\"table\") == \"benefits\"]\n",
"cost_rows = [v for v in values if v.get(\"table\") == \"costs\"]\n",
"\n",
"def value_table(rows, *, initial=False):\n",
" out = []\n",
" for v in rows:\n",
" yv = v.get(\"year_values\") or {}\n",
" rec = {\"field\": v.get(\"label\") or v[\"field_key\"]}\n",
" if initial:\n",
" rec[\"Initial\"] = v.get(\"initial\", 0.0)\n",
" rec.update({f\"Year {y}\": yv.get(str(y), 0.0) for y in (1, 2, 3)})\n",
" rec[\"risk_adj\"] = v.get(\"risk_adjustment\")\n",
" out.append(rec)\n",
" return pd.DataFrame(out)\n",
"\n",
"print(\"Benefits (nominal; Athena risk-adjusts at calculate time):\")\n",
"display(value_table(benefit_rows))\n",
"print(\"Costs (stored pre-multiplied by 1 + risk_adj):\")\n",
"display(value_table(cost_rows, initial=True))"
]
},
{
"cell_type": "markdown",
"id": "2383f761",
"metadata": {},
"source": [
"## Financial visualizations\n",
"\n",
"All figures are risk-adjusted and built from the live Athena values, using\n",
"the shared `core.notebook_helpers.charts` helpers (same ones the Streamlit\n",
"app uses)."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# ── Visual theme — plain values, edit freely (not confidential) ──\n",
"THEME = {\n",
" \"heading_font\": \"Helvetica Neue, Arial, sans-serif\", # chart titles\n",
" \"body_font\": \"Helvetica, Arial, sans-serif\", # axes, legends, labels\n",
" \"font_color\": \"#1F2937\", # hex\n",
"\n",
" # Circle-chart slice colours, applied in order a..j\n",
" \"pie_colors\": {\n",
" \"a\": \"#1565C0\",\n",
" \"b\": \"#2E7D32\",\n",
" \"c\": \"#C62828\",\n",
" \"d\": \"#F9A825\",\n",
" \"e\": \"#6A1B9A\",\n",
" \"f\": \"#00838F\",\n",
" \"g\": \"#EF6C00\",\n",
" \"h\": \"#5D4037\",\n",
" \"i\": \"#37474F\",\n",
" \"j\": \"#AD1457\",\n",
" },\n",
"\n",
" \"bar_green\": \"#2E7D32\", # benefits bars\n",
" \"bar_red\": \"#C62828\", # costs bars\n",
"}\n",
"\n",
"charts.apply_theme(**THEME)\n",
"print(\"Theme applied — re-run the chart cells below to restyle.\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "14d5942a",
"metadata": {},
"outputs": [],
"source": [
"# Cost breakdown — share of total three-year cost per line item\n",
"charts.cost_breakdown_pie(\n",
" cost_rows, title=\"Cost Breakdown — share of 3-year total (risk-adjusted)\"\n",
").show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d13f342e",
"metadata": {},
"outputs": [],
"source": [
"# Benefits — three-year risk-adjusted total per category\n",
"charts.benefits_bar(\n",
" benefit_rows, title=\"Benefits (Three-Year, Risk-Adjusted)\"\n",
").show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9ef6caee",
"metadata": {},
"outputs": [],
"source": [
"# Benefits vs costs, year by year (Initial = one-time Year-0 costs)\n",
"charts.benefits_vs_costs_by_year(benefit_rows, cost_rows).show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6f069df6",
"metadata": {},
"outputs": [],
"source": [
"# Cash flow with cumulative net benefits — the Forrester-style exhibit\n",
"def _yearly_breakdown(benefit_items, cost_items):\n",
" \"\"\"Risk-adjusted yearly rows + initial, computed from the live values.\"\"\"\n",
" initial = sum(float(c.get(\"initial\") or 0) for c in cost_items)\n",
" rows, cumulative = [], -initial\n",
" for y in (1, 2, 3):\n",
" b = sum(float((v.get(\"year_values\") or {}).get(str(y), 0) or 0)\n",
" * (1 - float(v.get(\"risk_adjustment\") or 0))\n",
" for v in benefit_items)\n",
" c = sum(float((v.get(\"year_values\") or {}).get(str(y), 0) or 0)\n",
" for v in cost_items)\n",
" cumulative += b - c\n",
" rows.append({\"year\": y, \"benefits\": b, \"costs\": c,\n",
" \"net\": b - c, \"cumulative_net\": cumulative})\n",
" return rows, initial\n",
"\n",
"yb, initial_cost = _yearly_breakdown(benefit_rows, cost_rows)\n",
"charts.cashflow_chart(yb, initial_cost=initial_cost).show()\n",
"pd.DataFrame(yb)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b1ff0315",
"metadata": {},
"outputs": [],
"source": [
"# Waterfall — Benefits PV down to NPV (from the Athena summary)\n",
"charts.waterfall([\n",
" (\"Benefits PV\", float(summary[\"total_benefits_pv\"])),\n",
" (\"Costs PV\", -float(summary[\"total_costs_pv\"])),\n",
" (\"NPV\", float(summary[\"net_present_value\"])),\n",
"], title=\"Benefits PV → Costs PV → NPV\").show()"
]
},
{
"cell_type": "markdown",
"id": "c78f77a9",
"metadata": {},
"source": [
"## Genesys AI Experience tokens — annual cost\n",
"\n",
"Enter the negotiated annual token cost from the Genesys quote. For sizing\n",
"context, the study's own drivers imply roughly **1,040,000** self-service\n",
"interactions/yr and **3,120,000** agent-assisted interactions/yr would draw\n",
"tokens — bring the actual figure from the quote, not a derivation."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "99b9c665",
"metadata": {},
"outputs": [],
"source": [
"# ── Deal inputs ──\n",
"AI_TOKEN_ANNUAL_COST = 0.0 # $/yr from the Genesys quote — 0 reproduces the published study\n",
"AI_TOKEN_QUOTE_NOTE = \"\" # e.g. \"Quote #1234: 4.2M tokens/yr @ $0.05, tier 2 commit\"\n",
"\n",
"print(f\"AI token line: ${AI_TOKEN_ANNUAL_COST:,.0f}/yr\")"
]
},
{
"cell_type": "markdown",
"id": "b7c6c4c7",
"metadata": {},
"source": [
"### Sensitivity — what the AI line does to NPV and ROI\n",
"\n",
"Computed locally from the current Athena summary: an annual cost `Δ` raises\n",
"costs PV by `Δ × 2.4869` (the 3-year, 10% annuity factor) and lowers NPV by\n",
"the same amount."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "09ceeea2",
"metadata": {},
"outputs": [],
"source": [
"ANNUITY = sum(1 / 1.10**n for n in (1, 2, 3)) # 2.4869\n",
"\n",
"base_benefits_pv = float(summary[\"total_benefits_pv\"])\n",
"base_costs_pv = float(summary[\"total_costs_pv\"])\n",
"\n",
"# Remove any token cost already stored, to get a clean zero-token base.\n",
"current_tokens = next(\n",
" (v for v in values if v[\"field_key\"] == \"genesys_ai_tokens\"), {})\n",
"current_annual = (current_tokens.get(\"year_values\") or {}).get(\"1\", 0.0)\n",
"base_costs_pv -= current_annual * ANNUITY\n",
"\n",
"sweep = [0, 100_000, 250_000, 500_000, 750_000, 1_000_000]\n",
"if AI_TOKEN_ANNUAL_COST and AI_TOKEN_ANNUAL_COST not in sweep:\n",
" sweep = sorted(sweep + [AI_TOKEN_ANNUAL_COST])\n",
"\n",
"rows = []\n",
"for ai_annual in sweep:\n",
" costs_pv = base_costs_pv + ai_annual * ANNUITY\n",
" npv = base_benefits_pv - costs_pv\n",
" rows.append({\n",
" \"AI cost/yr\": f\"${ai_annual:,.0f}\" + (\" ← your input\" if ai_annual == AI_TOKEN_ANNUAL_COST and ai_annual else \"\"),\n",
" \"Costs PV\": f\"${costs_pv:,.0f}\",\n",
" \"NPV\": f\"${npv:,.0f}\",\n",
" \"ROI\": f\"{npv / costs_pv * 100:,.0f}%\",\n",
" })\n",
"\n",
"display(pd.DataFrame(rows))"
]
},
{
"cell_type": "markdown",
"id": "6516270c",
"metadata": {},
"source": [
"## Push the AI token cost to Athena\n",
"\n",
"Writes the annual cost into `genesys_ai_tokens` (quote details in the notes),\n",
"recalculates server-side, and saves a version."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c6caacea",
"metadata": {},
"outputs": [],
"source": [
"PUSH = False # ← set True once AI_TOKEN_ANNUAL_COST is final\n",
"\n",
"if PUSH:\n",
" note = (\n",
" f\"AI Experience tokens: ${AI_TOKEN_ANNUAL_COST:,.0f}/yr. \"\n",
" + (f\"{AI_TOKEN_QUOTE_NOTE} \" if AI_TOKEN_QUOTE_NOTE else \"\")\n",
" + \"Line absent from the published Forrester study.\"\n",
" )\n",
" client.update_values(TOOL_ID, [{\n",
" \"field_key\": \"genesys_ai_tokens\",\n",
" \"year_values\": {\"1\": round(AI_TOKEN_ANNUAL_COST, 2),\n",
" \"2\": round(AI_TOKEN_ANNUAL_COST, 2),\n",
" \"3\": round(AI_TOKEN_ANNUAL_COST, 2)},\n",
" \"notes\": note,\n",
" }])\n",
" summary = client.calculate(TOOL_ID)\n",
" client.print_summary(TOOL_ID)\n",
" client.save_version(TOOL_ID, note=f\"AI token cost set: {note}\")\n",
" print(\"✅ Pushed, recalculated, and versioned. Re-run the visualization \"\n",
" \"cells above to refresh the charts.\")\n",
"else:\n",
" print(\"Dry run — set PUSH = True to write to Athena.\")"
]
},
{
"cell_type": "markdown",
"id": "c261ad48",
"metadata": {},
"source": [
"## Next steps\n",
"\n",
"- Adjust other drivers for the client (interaction volume, agent count,\n",
" self-service delta) via `client.update_values` or the Streamlit app\n",
" (`make app`), saving a version per scenario.\n",
"- Export charts for a deck: any figure object supports\n",
" `fig.write_image(\"chart.png\")` (needs `pip install kaleido`) or\n",
" `fig.write_html(\"chart.html\")`.\n",
"- Export for the report pipeline:\n",
" `python -m palladium export $PALLADIUM_GENESYSCX_TOOL_PUBLIC_ID -o exports/export.json`"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.7"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,382 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "g3-md-intro",
"metadata": {},
"source": [
"# 03 \u2014 Business Case\n",
"\n",
"Combine the benefits and costs into the consolidated TEI summary,\n",
"render the cash-flow exhibit, run scenario analysis, **and price the\n",
"Genesys AI Experience tokens line that the published study omits**.\n",
"This notebook should reproduce the headline numbers from the PDF\n",
"Financial Summary:\n",
"\n",
"* **NPV \\$10.78M \u2022 ROI 266% \u2022 Payback \u2248 4 months**\n",
"\n",
"It then exposes a sensitivity sweep for the AI-tokens annual cost so\n",
"you can see exactly what an honest deal looks like before sending it\n",
"to a client."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "03-bootstrap",
"metadata": {},
"outputs": [],
"source": [
"import sys, pathlib # path shim: works on a fresh kernel\n",
"for _p in [pathlib.Path.cwd(), *pathlib.Path.cwd().parents]:\n",
" if (_p / \"pyproject.toml\").exists():\n",
" sys.path.insert(0, str(_p)); break\n",
"\n",
"from core.bootstrap import init\n",
"\n",
"pal = init(study=\"202512_GenesysCX\")\n",
"client, seed, config = pal.client, pal.seed_data, pal.config\n",
"\n",
"STUDY = pal.root / 'studies' / '202512_GenesysCX'\n",
"ROOT = pal.root\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g3-code-imports",
"metadata": {},
"outputs": [],
"source": [
"from core.export.report_data import _compute_summary\n",
"from core.notebook_helpers import charts, display, tables"
]
},
{
"cell_type": "markdown",
"id": "g3-md-summary",
"metadata": {},
"source": [
"## Local summary (no Athena round-trip)\n",
"\n",
"Compute the moderate-case TEI summary directly from `seed_data` so the\n",
"notebook produces results even before the Athena tool is provisioned.\n",
"Headline numbers should match the published study."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g3-code-summary",
"metadata": {},
"outputs": [],
"source": [
"summary = _compute_summary(\n",
" seed.BENEFITS,\n",
" seed.COSTS,\n",
" config.DISCOUNT_RATE,\n",
" config.ANALYSIS_YEARS,\n",
")\n",
"# `_compute_summary` returns roi_pct; expose it as `roi` for kpi_cards.\n",
"summary['roi'] = summary.get('roi_pct')\n",
"display.kpi_cards(summary, title='Forrester composite \u2014 moderate case')"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g3-code-cashflow-table",
"metadata": {},
"outputs": [],
"source": [
"df_cash = tables.cashflow_table(summary)\n",
"df_cash.style.format({c: '${:,.0f}' for c in df_cash.columns if c != 'Year'})"
]
},
{
"cell_type": "markdown",
"id": "g3-md-cashflow",
"metadata": {},
"source": [
"## Cash flow chart\n",
"\n",
"Mirrors the Forrester *Cash Flow Chart* exhibit: stacked benefits/costs\n",
"by year + cumulative-net line. Payback hits inside Year 1."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g3-code-cashflow-chart",
"metadata": {},
"outputs": [],
"source": [
"charts.cashflow_chart(\n",
" summary['yearly_breakdown'],\n",
" initial_cost=summary.get('initial_costs', 0),\n",
").show()"
]
},
{
"cell_type": "markdown",
"id": "g3-md-waterfall",
"metadata": {},
"source": [
"## Waterfall: Benefits PV \u2192 Costs PV \u2192 NPV"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g3-code-waterfall",
"metadata": {},
"outputs": [],
"source": [
"charts.waterfall([\n",
" ('Benefits PV', summary['total_benefits_pv']),\n",
" ('Costs PV', -summary['total_costs_pv']),\n",
" ('NPV', summary['npv']),\n",
"]).show()"
]
},
{
"cell_type": "markdown",
"id": "g3-md-scenarios",
"metadata": {},
"source": [
"## Scenario analysis\n",
"\n",
"Apply the default Palladium multipliers (see `core.calculations.SCENARIOS`):\n",
"\n",
"* **Conservative** \u2014 lower adoption, higher risk on benefits / lower on costs\n",
"* **Moderate** \u2014 base case (= the published Forrester study)\n",
"* **Aggressive** \u2014 full adoption, lower risk on benefits / higher on costs"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g3-code-scenarios",
"metadata": {},
"outputs": [],
"source": [
"from core.calculations import apply_scenario\n",
"import pandas as pd\n",
"\n",
"scenario_summaries = {}\n",
"for name in ('conservative', 'moderate', 'aggressive'):\n",
" sb = apply_scenario(seed.BENEFITS, name, table='benefits')\n",
" sc = apply_scenario(seed.COSTS, name, table='costs')\n",
" scenario_summaries[name] = _compute_summary(sb, sc, config.DISCOUNT_RATE, config.ANALYSIS_YEARS)\n",
"\n",
"scen_df = pd.DataFrame([\n",
" {\n",
" 'Scenario': k,\n",
" 'Benefits PV': v['total_benefits_pv'],\n",
" 'Costs PV': v['total_costs_pv'],\n",
" 'NPV': v['npv'],\n",
" 'ROI %': v['roi_pct'],\n",
" 'Payback (mo)': round(v['payback_months'], 1) if v['payback_months'] is not None else None,\n",
" }\n",
" for k, v in scenario_summaries.items()\n",
"])\n",
"scen_df.style.format({\n",
" 'Benefits PV': '${:,.0f}', 'Costs PV': '${:,.0f}', 'NPV': '${:,.0f}', 'ROI %': '{:,.0f}%'\n",
"})"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g3-code-scenario-chart",
"metadata": {},
"outputs": [],
"source": [
"charts.scenario_comparison(scenario_summaries).show()"
]
},
{
"cell_type": "markdown",
"id": "g3-md-tokens-intro",
"metadata": {},
"source": [
"## Genesys AI Experience tokens \u2014 annual cost\n",
"\n",
"Token pricing is tiered, capability-dependent, and deal-specific \u2014\n",
"Athena stores a single annual cost value per line, and so does the\n",
"seed. Enter the negotiated annual cost from the Genesys quote here.\n",
"Quote details (volume, unit price, tier) go into the field notes for\n",
"the audit trail.\n",
"\n",
"For sizing context, the study's own drivers imply roughly **1,040,000**\n",
"self-service interactions/yr and **3,120,000** agent-assisted\n",
"interactions/yr would draw tokens \u2014 bring the actual figure from the\n",
"quote, not a derivation."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g3-code-token-input",
"metadata": {},
"outputs": [],
"source": [
"# \u2500\u2500 Deal inputs \u2500\u2500\n",
"AI_TOKEN_ANNUAL_COST = 0.0 # $/yr from the Genesys quote \u2014 0 reproduces the published study\n",
"AI_TOKEN_QUOTE_NOTE = \"\" # e.g. \"Quote #1234: 4.2M tokens/yr @ $0.05, tier 2 commit\"\n",
"\n",
"print(f'AI token line: ${AI_TOKEN_ANNUAL_COST:,.0f}/yr')"
]
},
{
"cell_type": "markdown",
"id": "g3-md-sensitivity",
"metadata": {},
"source": [
"### Sensitivity \u2014 what the AI line does to NPV and ROI\n",
"\n",
"An annual cost `\u0394` raises Costs PV by `\u0394 \u00d7 2.4869` (the 3-year, 10%\n",
"annuity factor) and lowers NPV by the same amount. The sweep below\n",
"shows where the deal stops being attractive \u2014 and quantifies how much\n",
"of the published 266% ROI was *contingent on Forrester modelling \\$0\n",
"of token spend*."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g3-code-sensitivity",
"metadata": {},
"outputs": [],
"source": [
"ANNUITY = sum(1 / 1.10**n for n in (1, 2, 3)) # 2.4869\n",
"\n",
"base_benefits_pv = float(summary['total_benefits_pv'])\n",
"base_costs_pv = float(summary['total_costs_pv'])\n",
"\n",
"sweep = [0, 100_000, 250_000, 500_000, 750_000, 1_000_000, 1_500_000, 2_000_000]\n",
"if AI_TOKEN_ANNUAL_COST and AI_TOKEN_ANNUAL_COST not in sweep:\n",
" sweep = sorted(sweep + [AI_TOKEN_ANNUAL_COST])\n",
"\n",
"rows = []\n",
"for ai_annual in sweep:\n",
" costs_pv = base_costs_pv + ai_annual * ANNUITY\n",
" npv_v = base_benefits_pv - costs_pv\n",
" roi_pct = (npv_v / costs_pv * 100) if costs_pv else 0\n",
" rows.append({\n",
" 'AI cost/yr': f\"${ai_annual:,.0f}\" + (' \u2190 your input' if ai_annual == AI_TOKEN_ANNUAL_COST and ai_annual else ''),\n",
" 'Costs PV': f'${costs_pv:,.0f}',\n",
" 'NPV': f'${npv_v:,.0f}',\n",
" 'ROI': f'{roi_pct:,.0f}%',\n",
" })\n",
"\n",
"pd.DataFrame(rows)"
]
},
{
"cell_type": "markdown",
"id": "g3-md-tokens-push",
"metadata": {},
"source": [
"### Push the AI-tokens cost to Athena\n",
"\n",
"When `AI_TOKEN_ANNUAL_COST` is set and `TOOL_PUBLIC_ID` exists, write\n",
"the annual cost into the `genesys_ai_tokens` field, with the quote\n",
"details preserved in the field notes."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g3-code-tokens-push",
"metadata": {},
"outputs": [],
"source": [
"PUSH = False # \u2190 set True once AI_TOKEN_ANNUAL_COST is final\n",
"\n",
"if PUSH and config.TOOL_PUBLIC_ID:\n",
" from core.tei_client import TEIClient\n",
"\n",
" note = (\n",
" f'AI Experience tokens: ${AI_TOKEN_ANNUAL_COST:,.0f}/yr. '\n",
" + (f'{AI_TOKEN_QUOTE_NOTE} ' if AI_TOKEN_QUOTE_NOTE else '')\n",
" + 'Line absent from the published Forrester study.'\n",
" )\n",
" client = TEIClient()\n",
" client.update_values(config.TOOL_PUBLIC_ID, [{\n",
" 'field_key': 'genesys_ai_tokens',\n",
" 'year_values': {'1': round(AI_TOKEN_ANNUAL_COST, 2),\n",
" '2': round(AI_TOKEN_ANNUAL_COST, 2),\n",
" '3': round(AI_TOKEN_ANNUAL_COST, 2)},\n",
" 'notes': note,\n",
" }])\n",
" client.calculate(config.TOOL_PUBLIC_ID)\n",
" client.print_summary(config.TOOL_PUBLIC_ID)\n",
" client.save_version(config.TOOL_PUBLIC_ID, note=f'AI token cost set: {note}')\n",
" display.alert('Pushed, recalculated, and versioned.', 'success')\n",
"else:\n",
" display.alert('Dry run \u2014 set <code>PUSH = True</code> and ensure '\n",
" '<code>TOOL_PUBLIC_ID</code> is configured to write to Athena.', 'info')"
]
},
{
"cell_type": "markdown",
"id": "g3-md-crosscheck",
"metadata": {},
"source": [
"## Cross-check vs Athena (optional)\n",
"\n",
"When `TOOL_PUBLIC_ID` is set, ask Athena to recalculate the summary on\n",
"the server side and confirm it matches our local computation (modulo\n",
"the documented Year-0 discounting delta \u2014 see `02_costs.ipynb`)."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g3-code-crosscheck",
"metadata": {},
"outputs": [],
"source": [
"if config.TOOL_PUBLIC_ID:\n",
" from core.tei_client import TEIClient\n",
"\n",
" client = TEIClient()\n",
" client.calculate(config.TOOL_PUBLIC_ID)\n",
" server_summary = client.get_summary(config.TOOL_PUBLIC_ID)\n",
" display.kpi_cards(server_summary, title='Athena server-side summary')\n",
"else:\n",
" display.alert('Set TOOL_PUBLIC_ID to compare Athena vs local.', 'info')"
]
},
{
"cell_type": "markdown",
"id": "g3-md-next",
"metadata": {},
"source": [
"Continue with [`04_export.ipynb`](04_export.ipynb) \u2192"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.7"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@@ -0,0 +1,195 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "g4-md-intro",
"metadata": {},
"source": [
"# 04 \u2014 Export for the report pipeline\n",
"\n",
"Build the structured JSON envelope consumed by the html2docx report\n",
"generation pipeline (Peitho). Output goes to `exports/export.json`."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "04-bootstrap",
"metadata": {},
"outputs": [],
"source": [
"import sys, pathlib # path shim: works on a fresh kernel\n",
"for _p in [pathlib.Path.cwd(), *pathlib.Path.cwd().parents]:\n",
" if (_p / \"pyproject.toml\").exists():\n",
" sys.path.insert(0, str(_p)); break\n",
"\n",
"from core.bootstrap import init\n",
"\n",
"pal = init(study=\"202512_GenesysCX\")\n",
"client, seed, config = pal.client, pal.seed_data, pal.config\n",
"\n",
"STUDY = pal.root / 'studies' / '202512_GenesysCX'\n",
"ROOT = pal.root\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g4-code-imports",
"metadata": {},
"outputs": [],
"source": [
"import json\n",
"from datetime import datetime, timezone\n",
"from core import __version__\n",
"from core.calculations import apply_scenario\n",
"from core.export.report_data import _compute_summary\n",
"from core.notebook_helpers import display"
]
},
{
"cell_type": "markdown",
"id": "g4-md-build",
"metadata": {},
"source": [
"## Build the envelope\n",
"\n",
"Two paths:\n",
"\n",
"* **Live** \u2014 `core.export.build_report_data(client, public_id)` pulls\n",
" authoritative values + summary from Athena and stamps it.\n",
"* **Local** \u2014 when no `TOOL_PUBLIC_ID` is configured, build the envelope\n",
" directly from `seed_data` so this notebook is always runnable."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g4-code-build",
"metadata": {},
"outputs": [],
"source": [
"if config.TOOL_PUBLIC_ID:\n",
" from core.export import build_report_data\n",
" from core.tei_client import TEIClient\n",
"\n",
" client = TEIClient()\n",
" envelope = build_report_data(\n",
" client,\n",
" config.TOOL_PUBLIC_ID,\n",
" include_scenarios=True,\n",
" study_slug=config.STUDY_SLUG,\n",
" )\n",
" source = 'live (Athena)'\n",
"else:\n",
" summary = _compute_summary(\n",
" seed.BENEFITS, seed.COSTS, config.DISCOUNT_RATE, config.ANALYSIS_YEARS\n",
" )\n",
" summary['roi'] = summary.get('roi_pct')\n",
" scenarios = {}\n",
" for name in ('conservative', 'moderate', 'aggressive'):\n",
" sb = apply_scenario(seed.BENEFITS, name, table='benefits')\n",
" sc = apply_scenario(seed.COSTS, name, table='costs')\n",
" scenarios[name] = _compute_summary(sb, sc, config.DISCOUNT_RATE, config.ANALYSIS_YEARS)\n",
" envelope = {\n",
" 'metadata': {\n",
" 'study_slug': config.STUDY_SLUG,\n",
" 'tool_public_id': '',\n",
" 'tool_name': 'CX Cloud (Genesys + Salesforce) TEI (local seed)',\n",
" 'report_name': 'Total Economic Impact\u2122 Of CX Cloud \u2014 Genesys + Salesforce',\n",
" 'report_vendor': 'Genesys',\n",
" 'report_version': '1.0',\n",
" 'generated_at': datetime.now(timezone.utc).isoformat(),\n",
" 'generator': f'palladium core {__version__} (offline)',\n",
" },\n",
" 'report': {\n",
" 'name': 'Total Economic Impact\u2122 Of CX Cloud \u2014 Genesys + Salesforce',\n",
" 'vendor': 'Genesys',\n",
" 'version': '1.0',\n",
" 'discount_rate': config.DISCOUNT_RATE,\n",
" 'analysis_period_years': config.ANALYSIS_YEARS,\n",
" },\n",
" 'values': {'benefits': seed.BENEFITS, 'costs': seed.COSTS},\n",
" 'summary': summary,\n",
" 'scenarios': scenarios,\n",
" 'assumptions': seed.ASSUMPTIONS,\n",
" }\n",
" source = 'offline seed data'\n",
"\n",
"display.alert(f'Envelope built from <b>{source}</b>.', 'info')"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g4-code-write",
"metadata": {},
"outputs": [],
"source": [
"out_path = STUDY / 'exports' / 'export.json'\n",
"out_path.parent.mkdir(parents=True, exist_ok=True)\n",
"out_path.write_text(json.dumps(envelope, indent=2, default=str))\n",
"size_kb = out_path.stat().st_size / 1024\n",
"display.alert(f'Wrote <code>{out_path.relative_to(ROOT)}</code> ({size_kb:.1f} KB).', 'success')"
]
},
{
"cell_type": "markdown",
"id": "g4-md-shape",
"metadata": {},
"source": [
"## Envelope shape\n",
"\n",
"Top-level keys consumed by the report pipeline:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g4-code-shape",
"metadata": {},
"outputs": [],
"source": [
"for key in envelope:\n",
" sub = envelope[key]\n",
" if isinstance(sub, dict):\n",
" print(f' {key}: dict with keys {list(sub.keys())}')\n",
" elif isinstance(sub, list):\n",
" print(f' {key}: list[{len(sub)}]')\n",
" else:\n",
" print(f' {key}: {type(sub).__name__}')"
]
},
{
"cell_type": "markdown",
"id": "g4-md-done",
"metadata": {},
"source": [
"Done. Hand off `exports/export.json` to **Peitho** / **html2docx** to produce the final Word report.\n",
"\n",
"**CLI alternative:** `python -m palladium export $PALLADIUM_GENESYSCX_TOOL_PUBLIC_ID -o studies/202512_GenesysCX/exports/export.json`"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.7"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long