studies/202512_GenesysCX -> studies/202512_TEI_Genesys_CX_Cloud, rebuilt as pattern Variant 4 (TEI composite reproduction): - teicalc/ self-contained engine: Forrester's tables as the never-edited verbatim anchor (incl. the p.14 typo note and the $0 AI-token line), generic model/scenarios/staging carried over from the Amazon Connect study, ClientDrivers overlay (agents / weekly interactions / revenue, flat composite so no growth re-base) with ai_tokens_annual as a direct input for the token line the published study models at $0 - one deliverable notebook (business_case.ipynb): widget-pair sidebar drivers incl. the AI-token price, published-vs-overlay KPI columns, cash-flow/waterfall/scenario charts, verification gate, backstage JSON data appendix - gate + tests reproduce the published totals within $2: NPV $10.8M / ROI 266% (engine $10,783,466 / 265.79%; payback 3.3 months, not headlined in the PDF); 29 study tests, headless nbconvert green, stage simulation leak-free, exports carry the appendix - old Athena workflow (00_provision..04_export, config.py, seed_data.py, PALLADIUM_GENESYSCX_* keys, ATHENA_EXPECTED reconciliation) deleted; git history preserves it With the last legacy study migrated, the retirement lands too: - app/ (Streamlit UI) and core/notebook_helpers deleted; nothing else imported them - streamlit stripped from pyproject extras, requirements.txt, Makefile; .env.example reduced to the Athena keys; 00_setup.ipynb and core/bootstrap.py repointed at the pattern studies - root README reworked: self-contained studies + slim core/ Athena toolkit (tei_client, calculations, export, cli) All suites green: Genesys 29, Amazon Connect 27, CTM 55, template 7, root 58. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
108 lines
4.5 KiB
Python
108 lines
4.5 KiB
Python
"""
|
||
Client overlay — Variant 4's personalization layer.
|
||
|
||
The verbatim anchor is Forrester's *composite organization* ($2.5B revenue,
|
||
600 CX agents, 80k weekly interactions). This module rescales that composite
|
||
to a client's size: a 🟡 **first-order linear rescale**, answering "what does
|
||
the composite look like at your scale?", not "what is your TEI?".
|
||
|
||
Each verbatim row is tied to the driver that dominates its derivation in the
|
||
PDF (see ``BENEFIT_DRIVERS``/``COST_DRIVERS``); rows scale linearly with
|
||
their driver, project-based costs stay fixed. This composite's trajectory is
|
||
flat (Y2 = Y3), so there is no growth re-base; linear scaling preserves the
|
||
legacy-retirement ramp shape.
|
||
|
||
The one non-ratio driver is ``ai_tokens_annual``: the published study models
|
||
**$0** Genesys AI Experience token consumption (see the anchor's footnote),
|
||
so a client case prices that line directly — the negotiated annual figure
|
||
from the Genesys quote replaces the row's year values outright.
|
||
|
||
``overlay_rows(COMPOSITE)`` is the identity — it reproduces the verbatim
|
||
numbers exactly (tokens included, at $0), so headless widget defaults form
|
||
the published-study reproduction the gate expects. The anchor is never
|
||
mutated: every function deep-copies.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from copy import deepcopy
|
||
from dataclasses import dataclass
|
||
|
||
from .anchor import ASSUMPTIONS, BENEFITS_VERBATIM, COSTS_VERBATIM
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ClientDrivers:
|
||
"""Client inputs; defaults are the Forrester composite (identity overlay)."""
|
||
|
||
agents_fte: int = ASSUMPTIONS["agents_fte"] # 600 (400 concurrent licenses)
|
||
weekly_interactions: int = ASSUMPTIONS["weekly_interactions"] # 80,000 @ 12 min
|
||
annual_revenue: float = ASSUMPTIONS["annual_revenue"] # $2.5B
|
||
ai_tokens_annual: float = 0.0 # 🔴 published study models $0 AI consumption
|
||
discount_rate: float = ASSUMPTIONS["discount_rate"] # 0.10
|
||
|
||
|
||
COMPOSITE = ClientDrivers()
|
||
|
||
|
||
#: 🟡 Which driver each verbatim row scales with, per its PDF derivation.
|
||
BENEFIT_DRIVERS: dict[str, str] = {
|
||
"legacy_retirement": "agents", # seat-scoped legacy platform costs
|
||
"self_service_savings": "interactions", # deflected volume → FTEs
|
||
"agent_efficiency": "interactions", # MTTR saving × handled volume
|
||
"agent_assist_sales": "revenue", # 20% of revenue × lift × margin
|
||
}
|
||
COST_DRIVERS: dict[str, str] = {
|
||
"cx_cloud_licenses": "agents", # 400 concurrent of 600 agents
|
||
"implementation": "fixed", # 10-week project — does not scale
|
||
"ongoing_management": "fixed", # small fixed team
|
||
"genesys_ai_tokens": "ai_tokens", # 🔴 direct annual input, not a ratio
|
||
}
|
||
|
||
|
||
def scale_factor(driver: str, d: ClientDrivers) -> float:
|
||
"""Linear size ratio vs the composite for one ratio-driver kind."""
|
||
if driver == "agents":
|
||
return d.agents_fte / ASSUMPTIONS["agents_fte"]
|
||
if driver == "interactions":
|
||
return d.weekly_interactions / ASSUMPTIONS["weekly_interactions"]
|
||
if driver == "revenue":
|
||
return d.annual_revenue / ASSUMPTIONS["annual_revenue"]
|
||
if driver == "fixed":
|
||
return 1.0
|
||
raise KeyError(f"Unknown driver: {driver!r}")
|
||
|
||
|
||
def overlay_rows(d: ClientDrivers = COMPOSITE) -> tuple[list[dict], list[dict]]:
|
||
"""
|
||
Deep-copied (benefits, costs) rows rescaled to the client's drivers.
|
||
|
||
Ratio-driven rows: ``year_values[n] ×= scale_factor(driver)`` (and
|
||
``initial`` likewise). Fixed rows are untouched. The ``ai_tokens`` row
|
||
takes ``d.ai_tokens_annual`` as each year's value directly — the
|
||
negotiated quote figure, not a rescale of the anchor's $0.
|
||
Risk factors, labels, and notes are unchanged everywhere.
|
||
"""
|
||
|
||
def _apply(rows: list[dict], drivers: dict[str, str]) -> list[dict]:
|
||
out = []
|
||
for raw in rows:
|
||
row = deepcopy(raw)
|
||
driver = drivers[row["field_key"]]
|
||
if driver == "ai_tokens":
|
||
row["year_values"] = {
|
||
k: float(d.ai_tokens_annual) for k in row["year_values"]
|
||
}
|
||
elif driver != "fixed":
|
||
s = scale_factor(driver, d)
|
||
row["year_values"] = {
|
||
k: float(v) * s for k, v in row["year_values"].items()
|
||
}
|
||
if row.get("initial"):
|
||
row["initial"] = float(row["initial"]) * s
|
||
out.append(row)
|
||
return out
|
||
|
||
return (_apply(BENEFITS_VERBATIM, BENEFIT_DRIVERS),
|
||
_apply(COSTS_VERBATIM, COST_DRIVERS))
|