Files
palladium/studies/202512_TEI_Genesys_CX_Cloud/teicalc/overlay.py
Robert Helewka e88449d15a Migrate Genesys CX Cloud TEI study to the pattern; retire Streamlit app
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>
2026-07-09 16:38:51 -04:00

108 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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))