feat: add GenesysCX study and fix Streamlit chart key collisions
- Add 202512_GenesysCX TEI study (config, seed data, notebooks, README) with NPV $10.8M / ROI 266% including AI-token cost line - Add explicit `key` parameter to all chart wrappers in app/components to prevent StreamlitDuplicateElementId errors when the same figure type renders across Summary/Benefits/Costs tabs - Render benefits bar and cost pie charts on their respective tabs - Add benefits_vs_costs_by_year chart wrapper
This commit is contained in:
929
studies/202512_GenesysCX/notebooks/00_provision.ipynb
Normal file
929
studies/202512_GenesysCX/notebooks/00_provision.ipynb
Normal file
@@ -0,0 +1,929 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "41520e77",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 00 · Provision — Genesys CX Cloud TEI in Athena\n",
|
||||
"\n",
|
||||
"Source study: Forrester, *The Total Economic Impact™ Of CX Cloud* (Genesys +\n",
|
||||
"Salesforce, December 2025). Published headline: **NPV \\$10.78M · ROI 266%**.\n",
|
||||
"\n",
|
||||
"This notebook creates everything the study needs in the Athena sandbox:\n",
|
||||
"\n",
|
||||
"1. **Report template** *CX Cloud (Genesys + Salesforce) 2025* + **field definitions** — 4 benefits, 3 published costs, **plus the `genesys_ai_tokens` consumption line the published study omits**\n",
|
||||
"2. **Client selection** from the CRM (profile pulled, no re-entry)\n",
|
||||
"3. **Attachment** to a Proposal or Engagement\n",
|
||||
"4. **Seed values** + server-side **calculation**\n",
|
||||
"5. **Two-tier verification**: exact match vs Athena-methodology expectations, then reconciliation to the published totals (explained Year-0 discounting delta)\n",
|
||||
"6. Persists study-scoped IDs (`PALLADIUM_GENESYSCX_*`) to `.env`\n",
|
||||
"\n",
|
||||
"Safe to re-run — every step finds existing objects before creating new ones."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "1b6f1117",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"✅ Athena connected — https://athena.ouranos.helu.ca (1 report templates visible)\n",
|
||||
"📁 Study: 202512_GenesysCX\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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, update_env\n",
|
||||
"\n",
|
||||
"pal = init(study=\"202512_GenesysCX\")\n",
|
||||
"client, seed, config = pal.client, pal.seed_data, pal.config\n",
|
||||
"assert pal.connection.get(\"status\") == \"ok\", \"Fix the connection first → 00_setup.ipynb\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "c1f8b6bd",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1 · Report template (find or create)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "cc81e408",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Created report template UCb2hSJprSBx\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"REPORT_NAME, VENDOR = \"CX Cloud (Genesys + Salesforce) 2025\", \"Genesys\"\n",
|
||||
"\n",
|
||||
"report = next(\n",
|
||||
" (r for r in client.list_reports()\n",
|
||||
" if r.get(\"name\") == REPORT_NAME and r.get(\"vendor\") == VENDOR),\n",
|
||||
" None,\n",
|
||||
")\n",
|
||||
"if report is None:\n",
|
||||
" report = client.create_report(\n",
|
||||
" name=REPORT_NAME,\n",
|
||||
" vendor=VENDOR,\n",
|
||||
" version=\"1.0\",\n",
|
||||
" description=(\n",
|
||||
" \"Forrester TEI of CX Cloud (Genesys + Salesforce), Dec 2025. \"\n",
|
||||
" \"Includes Palladium's genesys_ai_tokens consumption line, \"\n",
|
||||
" \"which the published study omits.\"\n",
|
||||
" ),\n",
|
||||
" analysis_period_years=seed.ASSUMPTIONS[\"analysis_years\"],\n",
|
||||
" discount_rate=seed.ASSUMPTIONS[\"discount_rate\"],\n",
|
||||
" status=\"draft\",\n",
|
||||
" )\n",
|
||||
" print(f\"Created report template {report['id']}\")\n",
|
||||
"else:\n",
|
||||
" print(f\"Found existing report template {report['id']} (status: {report.get('status')})\")\n",
|
||||
"\n",
|
||||
"REPORT_ID = report[\"id\"]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "e31bbd8b",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2 · Field definitions\n",
|
||||
"\n",
|
||||
"Same Palladium conventions as the Amazon Connect study: benefit risk\n",
|
||||
"adjustments live on the field; cost values get pushed pre-multiplied by\n",
|
||||
"`(1 + risk_adj)`; Year-0 amounts use companion `*_initial` fields.\n",
|
||||
"The `genesys_ai_tokens` line is seeded \\$0 (reproduces the published study) —\n",
|
||||
"the annual cost gets entered per deal, from the Genesys quote, in\n",
|
||||
"`01_business_case.ipynb`."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "55e69828",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"12 fields created, 0 already existed.\n",
|
||||
"Report template activated.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"def field_defs():\n",
|
||||
" defs, sort = [], 0\n",
|
||||
" for b in seed.BENEFITS:\n",
|
||||
" sort += 1\n",
|
||||
" defs.append({\n",
|
||||
" \"table\": \"benefits\",\n",
|
||||
" \"field_key\": b[\"field_key\"],\n",
|
||||
" \"label\": b[\"label\"],\n",
|
||||
" \"description\": b[\"notes\"][:200],\n",
|
||||
" \"field_type\": \"currency\",\n",
|
||||
" \"category\": b[\"category\"],\n",
|
||||
" \"is_annual\": True,\n",
|
||||
" \"risk_adjustment\": str(b[\"risk_adjustment\"]),\n",
|
||||
" \"sort_order\": sort,\n",
|
||||
" \"is_required\": True,\n",
|
||||
" \"source_notes\": b[\"notes\"],\n",
|
||||
" })\n",
|
||||
" for c in seed.COSTS:\n",
|
||||
" sort += 1\n",
|
||||
" defs.append({\n",
|
||||
" \"table\": \"costs\",\n",
|
||||
" \"field_key\": c[\"field_key\"],\n",
|
||||
" \"label\": c[\"label\"],\n",
|
||||
" \"description\": c[\"notes\"][:200],\n",
|
||||
" \"field_type\": \"currency\",\n",
|
||||
" \"category\": c[\"category\"],\n",
|
||||
" \"is_annual\": True,\n",
|
||||
" \"risk_adjustment\": \"0\", # cost risk adj applied client-side\n",
|
||||
" \"sort_order\": sort,\n",
|
||||
" \"is_required\": False,\n",
|
||||
" \"source_notes\": c[\"notes\"],\n",
|
||||
" })\n",
|
||||
" sort += 1\n",
|
||||
" defs.append({\n",
|
||||
" \"table\": \"costs\",\n",
|
||||
" \"field_key\": f\"{c['field_key']}_initial\",\n",
|
||||
" \"label\": f\"{c['label']} — initial (Year 0)\",\n",
|
||||
" \"description\": \"One-time Year-0 amount (companion field).\",\n",
|
||||
" \"field_type\": \"currency\",\n",
|
||||
" \"category\": c[\"category\"],\n",
|
||||
" \"is_annual\": False,\n",
|
||||
" \"risk_adjustment\": \"0\",\n",
|
||||
" \"sort_order\": sort,\n",
|
||||
" \"is_required\": False,\n",
|
||||
" \"source_notes\": \"Year-0 lump sum; Athena treats non-annual values as Year 1.\",\n",
|
||||
" })\n",
|
||||
" return defs\n",
|
||||
"\n",
|
||||
"existing = {f[\"field_key\"] for f in client.list_fields(REPORT_ID)}\n",
|
||||
"created = 0\n",
|
||||
"for d in field_defs():\n",
|
||||
" if d[\"field_key\"] not in existing:\n",
|
||||
" client.create_field(REPORT_ID, d)\n",
|
||||
" created += 1\n",
|
||||
"print(f\"{created} fields created, {len(existing)} already existed.\")\n",
|
||||
"\n",
|
||||
"if report.get(\"status\") == \"draft\":\n",
|
||||
" client.update_report(REPORT_ID, status=\"active\")\n",
|
||||
" print(\"Report template activated.\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "96b360d3",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3 · Select the client"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "5a0a701f",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<div>\n",
|
||||
"<style scoped>\n",
|
||||
" .dataframe tbody tr th:only-of-type {\n",
|
||||
" vertical-align: middle;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe tbody tr th {\n",
|
||||
" vertical-align: top;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe thead th {\n",
|
||||
" text-align: right;\n",
|
||||
" }\n",
|
||||
"</style>\n",
|
||||
"<table border=\"1\" class=\"dataframe\">\n",
|
||||
" <thead>\n",
|
||||
" <tr style=\"text-align: right;\">\n",
|
||||
" <th></th>\n",
|
||||
" <th>id</th>\n",
|
||||
" <th>name</th>\n",
|
||||
" <th>vertical</th>\n",
|
||||
" <th>client_type</th>\n",
|
||||
" <th>employee_count</th>\n",
|
||||
" <th>contact_center_agent_count</th>\n",
|
||||
" <th>supervisor_count</th>\n",
|
||||
" </tr>\n",
|
||||
" </thead>\n",
|
||||
" <tbody>\n",
|
||||
" <tr>\n",
|
||||
" <th>0</th>\n",
|
||||
" <td>2</td>\n",
|
||||
" <td>Global Guardian Insurance</td>\n",
|
||||
" <td>None</td>\n",
|
||||
" <td>For-Profit</td>\n",
|
||||
" <td>12000</td>\n",
|
||||
" <td>2500</td>\n",
|
||||
" <td>None</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>1</th>\n",
|
||||
" <td>3</td>\n",
|
||||
" <td>Eudaimonix</td>\n",
|
||||
" <td>None</td>\n",
|
||||
" <td>For-Profit</td>\n",
|
||||
" <td>1500</td>\n",
|
||||
" <td>300</td>\n",
|
||||
" <td>None</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>2</th>\n",
|
||||
" <td>4</td>\n",
|
||||
" <td>Aetherium Forge</td>\n",
|
||||
" <td>None</td>\n",
|
||||
" <td>For-Profit</td>\n",
|
||||
" <td>500</td>\n",
|
||||
" <td>42</td>\n",
|
||||
" <td>None</td>\n",
|
||||
" </tr>\n",
|
||||
" </tbody>\n",
|
||||
"</table>\n",
|
||||
"</div>"
|
||||
],
|
||||
"text/plain": [
|
||||
" id name vertical client_type employee_count \\\n",
|
||||
"0 2 Global Guardian Insurance None For-Profit 12000 \n",
|
||||
"1 3 Eudaimonix None For-Profit 1500 \n",
|
||||
"2 4 Aetherium Forge None For-Profit 500 \n",
|
||||
"\n",
|
||||
" contact_center_agent_count supervisor_count \n",
|
||||
"0 2500 None \n",
|
||||
"1 300 None \n",
|
||||
"2 42 None "
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"CLIENT_SEARCH = \"\" # e.g. \"Acme\" — empty lists everyone\n",
|
||||
"\n",
|
||||
"clients = client.list_clients(search=CLIENT_SEARCH or None)\n",
|
||||
"if clients:\n",
|
||||
" display(pd.DataFrame(clients)[\n",
|
||||
" [c for c in (\"id\", \"name\", \"vertical\", \"client_type\", \"employee_count\",\n",
|
||||
" \"contact_center_agent_count\", \"supervisor_count\")\n",
|
||||
" if c in clients[0]]\n",
|
||||
" ])\n",
|
||||
"else:\n",
|
||||
" print(\"No clients found — create one in the Athena UI (Orbit → Clients) and re-run.\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "1e375b54",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<div>\n",
|
||||
"<style scoped>\n",
|
||||
" .dataframe tbody tr th:only-of-type {\n",
|
||||
" vertical-align: middle;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe tbody tr th {\n",
|
||||
" vertical-align: top;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe thead th {\n",
|
||||
" text-align: right;\n",
|
||||
" }\n",
|
||||
"</style>\n",
|
||||
"<table border=\"1\" class=\"dataframe\">\n",
|
||||
" <thead>\n",
|
||||
" <tr style=\"text-align: right;\">\n",
|
||||
" <th></th>\n",
|
||||
" <th>Global Guardian Insurance</th>\n",
|
||||
" </tr>\n",
|
||||
" </thead>\n",
|
||||
" <tbody>\n",
|
||||
" <tr>\n",
|
||||
" <th>id</th>\n",
|
||||
" <td>2</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>name</th>\n",
|
||||
" <td>Global Guardian Insurance</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>abbreviated_name</th>\n",
|
||||
" <td>GGI</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>vertical</th>\n",
|
||||
" <td>None</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>client_type</th>\n",
|
||||
" <td>For-Profit</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>employee_count</th>\n",
|
||||
" <td>12000</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>revenue</th>\n",
|
||||
" <td>4500000000.0</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>contact_center_agent_count</th>\n",
|
||||
" <td>2500</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>service_desk_agent_count</th>\n",
|
||||
" <td>300</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>supervisor_count</th>\n",
|
||||
" <td>None</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>location_count</th>\n",
|
||||
" <td>120</td>\n",
|
||||
" </tr>\n",
|
||||
" </tbody>\n",
|
||||
"</table>\n",
|
||||
"</div>"
|
||||
],
|
||||
"text/plain": [
|
||||
" Global Guardian Insurance\n",
|
||||
"id 2\n",
|
||||
"name Global Guardian Insurance\n",
|
||||
"abbreviated_name GGI\n",
|
||||
"vertical None\n",
|
||||
"client_type For-Profit\n",
|
||||
"employee_count 12000\n",
|
||||
"revenue 4500000000.0\n",
|
||||
"contact_center_agent_count 2500\n",
|
||||
"service_desk_agent_count 300\n",
|
||||
"supervisor_count None\n",
|
||||
"location_count 120"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"CRM agent count: 2500 (composite: 600) — indicative scale 4.17×\n",
|
||||
"CRM revenue: $4,500,000,000 (composite: $2,500,000,000)\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"CLIENT_ID = 2 # ← set from the `id` column above, or leave for auto-pick\n",
|
||||
"\n",
|
||||
"if CLIENT_ID is None and len(clients) == 1:\n",
|
||||
" CLIENT_ID = clients[0][\"id\"]\n",
|
||||
" print(f\"Auto-selected the only client: {clients[0]['name']} (id={CLIENT_ID})\")\n",
|
||||
"assert CLIENT_ID is not None, \"Set CLIENT_ID from the table above and re-run this cell.\"\n",
|
||||
"\n",
|
||||
"profile = client.client_profile(CLIENT_ID)\n",
|
||||
"CLIENT_NAME = profile[\"name\"]\n",
|
||||
"display(pd.DataFrame([profile]).T.rename(columns={0: CLIENT_NAME}))\n",
|
||||
"\n",
|
||||
"# Client data → study scaling levers (no re-entry)\n",
|
||||
"CLIENT_ASSUMPTIONS = dict(seed.ASSUMPTIONS)\n",
|
||||
"if profile.get(\"contact_center_agent_count\"):\n",
|
||||
" CLIENT_ASSUMPTIONS[\"agents_fte\"] = profile[\"contact_center_agent_count\"]\n",
|
||||
" scale = CLIENT_ASSUMPTIONS[\"agents_fte\"] / seed.ASSUMPTIONS[\"agents_fte\"]\n",
|
||||
" print(f\"CRM agent count: {CLIENT_ASSUMPTIONS['agents_fte']} \"\n",
|
||||
" f\"(composite: {seed.ASSUMPTIONS['agents_fte']}) — \"\n",
|
||||
" f\"indicative scale {scale:.2f}×\")\n",
|
||||
"if profile.get(\"revenue\"):\n",
|
||||
" CLIENT_ASSUMPTIONS[\"annual_revenue\"] = float(profile[\"revenue\"])\n",
|
||||
" print(f\"CRM revenue: ${CLIENT_ASSUMPTIONS['annual_revenue']:,.0f} \"\n",
|
||||
" f\"(composite: ${seed.ASSUMPTIONS['annual_revenue']:,.0f})\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "2ff83486",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4 · Pick the attachment — Proposal or Engagement"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "584e01dd",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Proposals for Global Guardian Insurance:\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<div>\n",
|
||||
"<style scoped>\n",
|
||||
" .dataframe tbody tr th:only-of-type {\n",
|
||||
" vertical-align: middle;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe tbody tr th {\n",
|
||||
" vertical-align: top;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe thead th {\n",
|
||||
" text-align: right;\n",
|
||||
" }\n",
|
||||
"</style>\n",
|
||||
"<table border=\"1\" class=\"dataframe\">\n",
|
||||
" <thead>\n",
|
||||
" <tr style=\"text-align: right;\">\n",
|
||||
" <th></th>\n",
|
||||
" <th>id</th>\n",
|
||||
" <th>name</th>\n",
|
||||
" <th>status</th>\n",
|
||||
" <th>opportunity</th>\n",
|
||||
" </tr>\n",
|
||||
" </thead>\n",
|
||||
" <tbody>\n",
|
||||
" <tr>\n",
|
||||
" <th>0</th>\n",
|
||||
" <td>1</td>\n",
|
||||
" <td>Secure Cloud Infrastructure Modernization</td>\n",
|
||||
" <td>Draft</td>\n",
|
||||
" <td>Secure Cloud Infrastructure Modernization</td>\n",
|
||||
" </tr>\n",
|
||||
" </tbody>\n",
|
||||
"</table>\n",
|
||||
"</div>"
|
||||
],
|
||||
"text/plain": [
|
||||
" id name status \\\n",
|
||||
"0 1 Secure Cloud Infrastructure Modernization Draft \n",
|
||||
"\n",
|
||||
" opportunity \n",
|
||||
"0 Secure Cloud Infrastructure Modernization "
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"proposals = client.proposals_for_client(CLIENT_ID)\n",
|
||||
"engagements = client.engagements_for_client(CLIENT_NAME)\n",
|
||||
"\n",
|
||||
"if proposals:\n",
|
||||
" print(f\"Proposals for {CLIENT_NAME}:\")\n",
|
||||
" display(pd.DataFrame([\n",
|
||||
" {\"id\": p[\"id\"], \"name\": p.get(\"name\"), \"status\": p.get(\"status\"),\n",
|
||||
" \"opportunity\": (p.get(\"opportunity\") or {}).get(\"name\")}\n",
|
||||
" for p in proposals\n",
|
||||
" ]))\n",
|
||||
"if engagements:\n",
|
||||
" print(f\"Engagements for {CLIENT_NAME}:\")\n",
|
||||
" display(pd.DataFrame([\n",
|
||||
" {\"id\": e[\"id\"], \"name\": e.get(\"name\"), \"status\": e.get(\"status\")}\n",
|
||||
" for e in engagements\n",
|
||||
" ]))\n",
|
||||
"if not proposals and not engagements:\n",
|
||||
" print(f\"{CLIENT_NAME} has no proposals or engagements yet — \"\n",
|
||||
" \"the next cell can create a sandbox opportunity + proposal.\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"id": "e04b1676",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Auto-selected proposal 1: Secure Cloud Infrastructure Modernization\n",
|
||||
"Attaching via: {'proposal': 1}\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Set exactly ONE (ids from above). Leave both None to auto-pick — a single\n",
|
||||
"# existing option wins; otherwise a sandbox opportunity + proposal is created.\n",
|
||||
"PROPOSAL_ID = config.PROPOSAL_ID # or e.g. 42\n",
|
||||
"ENGAGEMENT_ID = config.ENGAGEMENT_ID # or e.g. 7\n",
|
||||
"\n",
|
||||
"if PROPOSAL_ID is None and ENGAGEMENT_ID is None:\n",
|
||||
" if len(proposals) == 1 and not engagements:\n",
|
||||
" PROPOSAL_ID = proposals[0][\"id\"]\n",
|
||||
" print(f\"Auto-selected proposal {PROPOSAL_ID}: {proposals[0].get('name')}\")\n",
|
||||
" elif len(engagements) == 1 and not proposals:\n",
|
||||
" ENGAGEMENT_ID = engagements[0][\"id\"]\n",
|
||||
" print(f\"Auto-selected engagement {ENGAGEMENT_ID}: {engagements[0].get('name')}\")\n",
|
||||
" elif not proposals and not engagements:\n",
|
||||
" opp = client.create_opportunity(\n",
|
||||
" name=f\"{CLIENT_NAME} — CX Cloud Modernization (sandbox)\",\n",
|
||||
" client_id=CLIENT_ID,\n",
|
||||
" description=\"Created by Palladium 00_provision for the Genesys CX Cloud TEI.\",\n",
|
||||
" )\n",
|
||||
" prop = client.create_proposal(\n",
|
||||
" name=f\"{CLIENT_NAME} — Genesys CX Cloud TEI (sandbox)\",\n",
|
||||
" opportunity_id=opp[\"id\"],\n",
|
||||
" status=\"Draft\",\n",
|
||||
" )\n",
|
||||
" PROPOSAL_ID = prop[\"id\"]\n",
|
||||
" print(f\"Created opportunity {opp['id']} and proposal {PROPOSAL_ID} for {CLIENT_NAME}.\")\n",
|
||||
" else:\n",
|
||||
" raise SystemExit(\"Multiple options — set PROPOSAL_ID or ENGAGEMENT_ID above and re-run.\")\n",
|
||||
"\n",
|
||||
"assert (PROPOSAL_ID is None) != (ENGAGEMENT_ID is None), \\\n",
|
||||
" \"Set exactly one of PROPOSAL_ID / ENGAGEMENT_ID.\"\n",
|
||||
"attach = {\"proposal\": PROPOSAL_ID} if PROPOSAL_ID else {\"engagement\": ENGAGEMENT_ID}\n",
|
||||
"print(f\"Attaching via: {attach}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "2b4fcb45",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5 · Tool instance & seed the published values"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"id": "0655d1fc",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Created tool 3rzDgVdsjhVv attached to {'proposal': 1}\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from core.tei_client import AthenaAPIError\n",
|
||||
"\n",
|
||||
"def _report_id_of(t):\n",
|
||||
" r = t.get(\"report\")\n",
|
||||
" return r.get(\"id\") if isinstance(r, dict) else r\n",
|
||||
"\n",
|
||||
"def _matches_attachment(t):\n",
|
||||
" if PROPOSAL_ID is not None:\n",
|
||||
" opp = t.get(\"opportunity\") or {}\n",
|
||||
" return t.get(\"proposal\") == PROPOSAL_ID or opp.get(\"proposal_id\") == PROPOSAL_ID\n",
|
||||
" eng = t.get(\"engagement\")\n",
|
||||
" eng_id = eng.get(\"id\") if isinstance(eng, dict) else eng\n",
|
||||
" return eng_id == ENGAGEMENT_ID\n",
|
||||
"\n",
|
||||
"candidates = [t for t in client.list_tools() if _report_id_of(t) == REPORT_ID]\n",
|
||||
"tool = next((t for t in candidates if _matches_attachment(t)),\n",
|
||||
" candidates[0] if len(candidates) == 1 else None)\n",
|
||||
"\n",
|
||||
"if tool is None:\n",
|
||||
" try:\n",
|
||||
" tool = client.create_tool(\n",
|
||||
" report_public_id=REPORT_ID,\n",
|
||||
" name=f\"{CLIENT_NAME} — Genesys CX Cloud TEI\",\n",
|
||||
" **attach,\n",
|
||||
" )\n",
|
||||
" print(f\"Created tool {tool['id']} attached to {attach}\")\n",
|
||||
" except AthenaAPIError as e:\n",
|
||||
" if e.status_code == 409: # DUPLICATE_INSTANCE\n",
|
||||
" raise SystemExit(\n",
|
||||
" \"An active tool already exists for this report + attachment. \"\n",
|
||||
" \"Find it with client.list_tools() or pick a different proposal/engagement.\"\n",
|
||||
" ) from e\n",
|
||||
" raise\n",
|
||||
"else:\n",
|
||||
" print(f\"Found existing tool {tool['id']} (status: {tool.get('status')})\")\n",
|
||||
"\n",
|
||||
"TOOL_ID = tool[\"id\"]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"id": "86443d76",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Pushed values for 8 fields (genesys_ai_tokens seeded at $0 — published-study baseline).\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"payload = []\n",
|
||||
"for b in seed.BENEFITS: # nominal; Athena risk-adjusts via the field definition\n",
|
||||
" payload.append({\n",
|
||||
" \"field_key\": b[\"field_key\"],\n",
|
||||
" \"year_values\": b[\"year_values\"],\n",
|
||||
" \"notes\": b[\"notes\"],\n",
|
||||
" })\n",
|
||||
"for c in seed.COSTS: # risk-adjusted UP client-side (Forrester methodology)\n",
|
||||
" factor = 1 + c[\"risk_adjustment\"]\n",
|
||||
" payload.append({\n",
|
||||
" \"field_key\": c[\"field_key\"],\n",
|
||||
" \"year_values\": {y: round(v * factor, 2) for y, v in c[\"year_values\"].items()},\n",
|
||||
" \"initial\": round(c[\"initial\"] * factor, 2),\n",
|
||||
" \"notes\": c[\"notes\"],\n",
|
||||
" })\n",
|
||||
"\n",
|
||||
"client.update_values(TOOL_ID, payload)\n",
|
||||
"print(f\"Pushed values for {len(payload)} fields \"\n",
|
||||
" f\"(genesys_ai_tokens seeded at $0 — published-study baseline).\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "509b52be",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6 · Calculate & verify\n",
|
||||
"\n",
|
||||
"**Tier 1 — pipeline correctness:** Athena must match `seed.ATHENA_EXPECTED`\n",
|
||||
"(the published model re-discounted under Athena's Year-0-as-Year-1 rule)\n",
|
||||
"within 0.5%.\n",
|
||||
"\n",
|
||||
"**Tier 2 — reconciliation:** show Athena vs the published totals. The\n",
|
||||
"implementation initial (\\$1.309M, ~32% of cost PV) is discounted by Athena\n",
|
||||
"but not by Forrester, so costs PV reads ~\\$119k lower and ROI ~11pp higher\n",
|
||||
"than published. That delta is methodology, not data error."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"id": "0728b42e",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"════════════════════════════════════════════════════════\n",
|
||||
" TEI Financial Summary\n",
|
||||
"════════════════════════════════════════════════════════\n",
|
||||
" Total Benefits (PV): $ 14,840,637\n",
|
||||
" Total Costs (PV): $ 3,938,170\n",
|
||||
"────────────────────────────────────────────────────────\n",
|
||||
" Net Present Value: $ 10,902,466\n",
|
||||
" ROI: 277%\n",
|
||||
" Payback: 4.0 months\n",
|
||||
"════════════════════════════════════════════════════════\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"summary = client.calculate(TOOL_ID)\n",
|
||||
"client.print_summary(TOOL_ID)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"id": "aba8fc21",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<div>\n",
|
||||
"<style scoped>\n",
|
||||
" .dataframe tbody tr th:only-of-type {\n",
|
||||
" vertical-align: middle;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe tbody tr th {\n",
|
||||
" vertical-align: top;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe thead th {\n",
|
||||
" text-align: right;\n",
|
||||
" }\n",
|
||||
"</style>\n",
|
||||
"<table border=\"1\" class=\"dataframe\">\n",
|
||||
" <thead>\n",
|
||||
" <tr style=\"text-align: right;\">\n",
|
||||
" <th></th>\n",
|
||||
" <th>metric</th>\n",
|
||||
" <th>published (Forrester)</th>\n",
|
||||
" <th>expected (Athena methodology)</th>\n",
|
||||
" <th>athena actual</th>\n",
|
||||
" <th>vs expected</th>\n",
|
||||
" </tr>\n",
|
||||
" </thead>\n",
|
||||
" <tbody>\n",
|
||||
" <tr>\n",
|
||||
" <th>0</th>\n",
|
||||
" <td>total_benefits_pv</td>\n",
|
||||
" <td>14,840,638</td>\n",
|
||||
" <td>14,840,640</td>\n",
|
||||
" <td>14,840,637</td>\n",
|
||||
" <td>-0.00%</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>1</th>\n",
|
||||
" <td>total_costs_pv</td>\n",
|
||||
" <td>4,057,170</td>\n",
|
||||
" <td>3,938,170</td>\n",
|
||||
" <td>3,938,170</td>\n",
|
||||
" <td>+0.00%</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>2</th>\n",
|
||||
" <td>net_present_value</td>\n",
|
||||
" <td>10,783,468</td>\n",
|
||||
" <td>10,902,470</td>\n",
|
||||
" <td>10,902,466</td>\n",
|
||||
" <td>-0.00%</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>3</th>\n",
|
||||
" <td>roi_percentage</td>\n",
|
||||
" <td>266</td>\n",
|
||||
" <td>277</td>\n",
|
||||
" <td>277</td>\n",
|
||||
" <td>+0.01%</td>\n",
|
||||
" </tr>\n",
|
||||
" </tbody>\n",
|
||||
"</table>\n",
|
||||
"</div>"
|
||||
],
|
||||
"text/plain": [
|
||||
" metric published (Forrester) expected (Athena methodology) \\\n",
|
||||
"0 total_benefits_pv 14,840,638 14,840,640 \n",
|
||||
"1 total_costs_pv 4,057,170 3,938,170 \n",
|
||||
"2 net_present_value 10,783,468 10,902,470 \n",
|
||||
"3 roi_percentage 266 277 \n",
|
||||
"\n",
|
||||
" athena actual vs expected \n",
|
||||
"0 14,840,637 -0.00% \n",
|
||||
"1 3,938,170 +0.00% \n",
|
||||
"2 10,902,466 -0.00% \n",
|
||||
"3 277 +0.01% "
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Payback: 4 months (expected ≈ 4)\n",
|
||||
"✅ Tier 1 passed — pipeline reproduces the study under Athena's discounting.\n",
|
||||
"ℹ️ Tier 2: published ROI 266% vs Athena ~277% — explained Year-0 delta (see above).\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"rows, ok = [], True\n",
|
||||
"for key in (\"total_benefits_pv\", \"total_costs_pv\", \"net_present_value\", \"roi_percentage\"):\n",
|
||||
" actual = float(summary.get(key) or 0)\n",
|
||||
" expected = seed.ATHENA_EXPECTED[key]\n",
|
||||
" published = seed.PUBLISHED[key]\n",
|
||||
" diff = (actual - expected) / expected\n",
|
||||
" rows.append({\n",
|
||||
" \"metric\": key,\n",
|
||||
" \"published (Forrester)\": f\"{published:,.0f}\",\n",
|
||||
" \"expected (Athena methodology)\": f\"{expected:,.0f}\",\n",
|
||||
" \"athena actual\": f\"{actual:,.0f}\",\n",
|
||||
" \"vs expected\": f\"{diff:+.2%}\",\n",
|
||||
" })\n",
|
||||
" ok &= abs(diff) <= 0.005\n",
|
||||
"\n",
|
||||
"display(pd.DataFrame(rows))\n",
|
||||
"print(f\"Payback: {summary.get('payback_period_months')} months (expected ≈ 4)\")\n",
|
||||
"assert ok, \"Athena diverged >0.5% from its own expected methodology — investigate.\"\n",
|
||||
"print(\"✅ Tier 1 passed — pipeline reproduces the study under Athena's discounting.\")\n",
|
||||
"print(\"ℹ️ Tier 2: published ROI 266% vs Athena ~277% — explained Year-0 delta (see above).\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "181c7b55",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7 · Save a baseline version & persist IDs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 13,
|
||||
"id": "d8102590",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"if not client.list_versions(TOOL_ID):\n",
|
||||
" 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",
|
||||
" ))\n",
|
||||
" print(\"Saved version 1 (baseline).\")\n",
|
||||
"\n",
|
||||
"ids = {\n",
|
||||
" \"PALLADIUM_GENESYSCX_REPORT_PUBLIC_ID\": REPORT_ID,\n",
|
||||
" \"PALLADIUM_GENESYSCX_TOOL_PUBLIC_ID\": TOOL_ID,\n",
|
||||
"}\n",
|
||||
"if PROPOSAL_ID is not None:\n",
|
||||
" ids[\"PALLADIUM_GENESYSCX_PROPOSAL_ID\"] = str(PROPOSAL_ID)\n",
|
||||
"if ENGAGEMENT_ID is not None:\n",
|
||||
" ids[\"PALLADIUM_GENESYSCX_ENGAGEMENT_ID\"] = str(ENGAGEMENT_ID)\n",
|
||||
"\n",
|
||||
"env_path = update_env(**ids)\n",
|
||||
"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).\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "4fc81c99-f073-486a-9f65-f207e96e59cd",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"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
|
||||
}
|
||||
376
studies/202512_GenesysCX/notebooks/01_business_case.ipynb
Normal file
376
studies/202512_GenesysCX/notebooks/01_business_case.ipynb
Normal file
@@ -0,0 +1,376 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
Reference in New Issue
Block a user