Token Calculator
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -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 + (1−bot_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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 == "🟢"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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},
|
||||
),
|
||||
}
|
||||
|
||||
56
studies/202512_GenesysCX/docs/Genesys-Token-Metering.md
Normal file
56
studies/202512_GenesysCX/docs/Genesys-Token-Metering.md
Normal 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 product’s 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.
|
||||
@@ -32,7 +32,7 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -69,7 +69,7 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"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",
|
||||
"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",
|
||||
"`01_business_case.ipynb`."
|
||||
"`03_business_case.ipynb`."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -127,8 +127,7 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"12 fields created, 0 already existed.\n",
|
||||
"Report template activated.\n"
|
||||
"0 fields created, 12 already existed.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -305,7 +304,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"execution_count": 5,
|
||||
"id": "1e375b54",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -445,7 +444,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"execution_count": 6,
|
||||
"id": "584e01dd",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -531,7 +530,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"execution_count": 7,
|
||||
"id": "e04b1676",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -539,7 +538,6 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Auto-selected proposal 1: Secure Cloud Infrastructure Modernization\n",
|
||||
"Attaching via: {'proposal': 1}\n"
|
||||
]
|
||||
}
|
||||
@@ -589,7 +587,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"execution_count": 8,
|
||||
"id": "0655d1fc",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -597,7 +595,7 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Created tool 3rzDgVdsjhVv attached to {'proposal': 1}\n"
|
||||
"Found existing tool 3rzDgVdsjhVv (status: draft)\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -643,7 +641,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"execution_count": 9,
|
||||
"id": "86443d76",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -696,7 +694,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"execution_count": 10,
|
||||
"id": "0728b42e",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -724,7 +722,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"execution_count": 11,
|
||||
"id": "aba8fc21",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -853,7 +851,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 13,
|
||||
"execution_count": 12,
|
||||
"id": "d8102590",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -861,13 +859,12 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Saved version 1 (baseline).\n",
|
||||
"Saved to /Users/robert/git/palladium/.env:\n",
|
||||
" PALLADIUM_GENESYSCX_REPORT_PUBLIC_ID=UCb2hSJprSBx\n",
|
||||
" PALLADIUM_GENESYSCX_TOOL_PUBLIC_ID=3rzDgVdsjhVv\n",
|
||||
" PALLADIUM_GENESYSCX_PROPOSAL_ID=1\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",
|
||||
" \"Baseline — published Forrester CX Cloud TEI figures (Dec 2025). \"\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",
|
||||
" print(\"Saved version 1 (baseline).\")\n",
|
||||
"\n",
|
||||
@@ -893,7 +890,7 @@
|
||||
"print(f\"Saved to {env_path}:\")\n",
|
||||
"for k, v in ids.items():\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": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "13acdc34-71f6-4220-8675-4e1527cb8e39",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
|
||||
1408
studies/202512_GenesysCX/notebooks/01_benefits.ipynb
Normal file
1408
studies/202512_GenesysCX/notebooks/01_benefits.ipynb
Normal file
File diff suppressed because one or more lines are too long
@@ -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
|
||||
}
|
||||
1508
studies/202512_GenesysCX/notebooks/02_costs.ipynb
Normal file
1508
studies/202512_GenesysCX/notebooks/02_costs.ipynb
Normal file
File diff suppressed because one or more lines are too long
382
studies/202512_GenesysCX/notebooks/03_business_case.ipynb
Normal file
382
studies/202512_GenesysCX/notebooks/03_business_case.ipynb
Normal 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
|
||||
}
|
||||
195
studies/202512_GenesysCX/notebooks/04_export.ipynb
Normal file
195
studies/202512_GenesysCX/notebooks/04_export.ipynb
Normal 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
Reference in New Issue
Block a user