Add configurable locale/display formatting environment variables (`PALLADIUM_CURRENCY_SYMBOL`, `PALLADIUM_THOUSANDS_SEP`, `PALLADIUM_DECIMAL_SEP`) to support regional number formatting in the Streamlit app. Update `.env.example` with documentation for these new variables. Also refresh `00_setup.ipynb` with current execution outputs reflecting a live Athena connection with report templates, a selected client (Global Guardian Insurance, ID=2), and resolved NameError in assumption override cells.
1005 lines
34 KiB
Plaintext
1005 lines
34 KiB
Plaintext
{
|
||
"cells": [
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "2b0c5d04",
|
||
"metadata": {},
|
||
"source": [
|
||
"# 00 · Provision — Amazon Connect TEI in Athena\n",
|
||
"\n",
|
||
"Creates everything this study needs in the Athena sandbox, end to end:\n",
|
||
"\n",
|
||
"1. **Report template** *Amazon Connect 2026* (3 years, 10% discount rate) + **field definitions**\n",
|
||
"2. **Client selection** — browse the CRM, pick the client, and pull their profile (industry, agent counts, revenue) so nothing is re-entered\n",
|
||
"3. **Attachment** — pick (or create) the **Proposal or Engagement** the tool binds to\n",
|
||
"4. **Tool instance** + **seed values** from `seed_data.py` (the published Forrester figures)\n",
|
||
"5. **Server-side calculation** and **verification** against the published totals: **NPV \\$78.7M · ROI 342% · payback <6 months**\n",
|
||
"6. Persists all IDs to `.env` so the other notebooks, the CLI, and the Streamlit app pick them up automatically.\n",
|
||
"\n",
|
||
"Safe to re-run — every step finds existing objects before creating new ones."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 2,
|
||
"id": "5bcc7740",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"✅ Athena connected — https://athena.ouranos.helu.ca (1 report templates visible)\n",
|
||
"📁 Study: 202602_AmazonConnect\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=\"202602_AmazonConnect\")\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": "cc8a4e03",
|
||
"metadata": {},
|
||
"source": [
|
||
"## 1 · Report template (find or create)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 3,
|
||
"id": "386ae38b",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Found existing report template xsUTbjh4iDnJ (status: active)\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"REPORT_NAME, VENDOR = \"Amazon Connect 2026\", \"AWS\"\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=\"Forrester Total Economic Impact of Amazon Connect, Feb 2026\",\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": "dab83777",
|
||
"metadata": {},
|
||
"source": [
|
||
"## 2 · Field definitions\n",
|
||
"\n",
|
||
"Derived straight from `seed_data.py`. Three methodology notes:\n",
|
||
"\n",
|
||
"- **Benefit risk adjustment** lives on the field definition — Athena applies `value × (1 − risk_adj)` at calculate time.\n",
|
||
"- **Costs**: Athena never risk-adjusts costs, but Forrester adjusts them *upward*. We therefore push cost values pre-multiplied by `(1 + risk_adj)` in step 5, and keep the field-level adjustment at 0 so nothing is applied twice.\n",
|
||
"- **Year-0 \"Initial\" amounts** have no native slot in the TEI API, so each cost gets a companion non-annual `<key>_initial` field. The client folds these back automatically on read."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 4,
|
||
"id": "dc46ab46",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"0 fields created, 11 already existed.\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\", # applied client-side, see note above\n",
|
||
" \"sort_order\": sort,\n",
|
||
" \"is_required\": True,\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": "d5841b4e",
|
||
"metadata": {},
|
||
"source": [
|
||
"## 3 · Select the client\n",
|
||
"\n",
|
||
"Browse the CRM. Adjust `CLIENT_SEARCH` to narrow the list, then set `CLIENT_ID`\n",
|
||
"below (it auto-selects when exactly one client matches)."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 5,
|
||
"id": "4070b9c2",
|
||
"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": "4e97978c",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"\n",
|
||
"Client profile — no re-entry needed downstream:\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>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"
|
||
}
|
||
],
|
||
"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",
|
||
"print(f\"\\nClient profile — no re-entry needed downstream:\")\n",
|
||
"display(pd.DataFrame([profile]).T.rename(columns={0: CLIENT_NAME}))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "0ae76599",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Client data → study assumptions\n",
|
||
"\n",
|
||
"Where the CRM has real numbers, they override the Forrester composite\n",
|
||
"(2,000 agents / 200 supervisors). `CLIENT_ASSUMPTIONS` is what `01_benefits.ipynb`\n",
|
||
"uses for scaling discussions."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 7,
|
||
"id": "fcccc591",
|
||
"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>assumption</th>\n",
|
||
" <th>Forrester composite</th>\n",
|
||
" <th>Global Guardian Insurance (CRM)</th>\n",
|
||
" </tr>\n",
|
||
" </thead>\n",
|
||
" <tbody>\n",
|
||
" <tr>\n",
|
||
" <th>0</th>\n",
|
||
" <td>agents_fte</td>\n",
|
||
" <td>2000</td>\n",
|
||
" <td>2500</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"</div>"
|
||
],
|
||
"text/plain": [
|
||
" assumption Forrester composite Global Guardian Insurance (CRM)\n",
|
||
"0 agents_fte 2000 2500"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
},
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Indicative scale factor vs composite: 1.25× (apply judgement — benefits don't all scale linearly)\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"CLIENT_ASSUMPTIONS = dict(seed.ASSUMPTIONS)\n",
|
||
"overrides = {\n",
|
||
" \"agents_fte\": profile.get(\"contact_center_agent_count\"),\n",
|
||
" \"supervisors_fte\": profile.get(\"supervisor_count\"),\n",
|
||
"}\n",
|
||
"rows = []\n",
|
||
"for key, val in overrides.items():\n",
|
||
" if val:\n",
|
||
" rows.append({\"assumption\": key, \"Forrester composite\": seed.ASSUMPTIONS[key],\n",
|
||
" f\"{CLIENT_NAME} (CRM)\": val})\n",
|
||
" CLIENT_ASSUMPTIONS[key] = val\n",
|
||
"\n",
|
||
"if rows:\n",
|
||
" display(pd.DataFrame(rows))\n",
|
||
" scale = CLIENT_ASSUMPTIONS[\"agents_fte\"] / seed.ASSUMPTIONS[\"agents_fte\"]\n",
|
||
" print(f\"Indicative scale factor vs composite: {scale:.2f}× \"\n",
|
||
" f\"(apply judgement — benefits don't all scale linearly)\")\n",
|
||
"else:\n",
|
||
" print(\"CRM has no agent/supervisor counts for this client — using the \"\n",
|
||
" \"Forrester composite organization as-is.\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "422c2ed6",
|
||
"metadata": {},
|
||
"source": [
|
||
"## 4 · Pick the attachment — Proposal or Engagement\n",
|
||
"\n",
|
||
"A TEI tool must attach to exactly one of the two. Both lists below are\n",
|
||
"already filtered to the selected client."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 8,
|
||
"id": "57dec6cf",
|
||
"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",
|
||
" <th>due_date</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",
|
||
" <td>2026-08-28</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 due_date \n",
|
||
"0 Secure Cloud Infrastructure Modernization 2026-08-28 "
|
||
]
|
||
},
|
||
"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",
|
||
" \"due_date\": p.get(\"due_date\")}\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",
|
||
" \"start\": e.get(\"start_date\"), \"end\": e.get(\"end_date\")}\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": 9,
|
||
"id": "19336bcc",
|
||
"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 of these (ids from the tables above). Leave both None to\n",
|
||
"# auto-pick — single existing proposal/engagement wins; otherwise a sandbox\n",
|
||
"# opportunity + proposal is created for the client.\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 Transformation (sandbox)\",\n",
|
||
" client_id=CLIENT_ID,\n",
|
||
" description=\"Created by Palladium 00_provision for the Amazon Connect TEI.\",\n",
|
||
" )\n",
|
||
" prop = client.create_proposal(\n",
|
||
" name=f\"{CLIENT_NAME} — Amazon Connect 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": "0056bce9",
|
||
"metadata": {},
|
||
"source": [
|
||
"## 5 · Tool instance (find or create) & seed the published values"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 10,
|
||
"id": "017ae9db",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Created tool pkrsQ9SRf654 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} — Amazon Connect 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": 11,
|
||
"id": "20e2a736",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Pushed values for 8 fields.\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.\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "697794fd",
|
||
"metadata": {},
|
||
"source": [
|
||
"## 6 · Calculate & verify against the published study"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 12,
|
||
"id": "b7ac5d24",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"════════════════════════════════════════════════════════\n",
|
||
" TEI Financial Summary\n",
|
||
"════════════════════════════════════════════════════════\n",
|
||
" Total Benefits (PV): $ 101,696,568\n",
|
||
" Total Costs (PV): $ 22,874,326\n",
|
||
"────────────────────────────────────────────────────────\n",
|
||
" Net Present Value: $ 78,822,242\n",
|
||
" ROI: 345%\n",
|
||
" Payback: 1.0 months\n",
|
||
"════════════════════════════════════════════════════════\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"summary = client.calculate(TOOL_ID)\n",
|
||
"client.print_summary(TOOL_ID)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 13,
|
||
"id": "13d84001",
|
||
"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</th>\n",
|
||
" <th>athena</th>\n",
|
||
" <th>diff</th>\n",
|
||
" </tr>\n",
|
||
" </thead>\n",
|
||
" <tbody>\n",
|
||
" <tr>\n",
|
||
" <th>0</th>\n",
|
||
" <td>total_benefits_pv</td>\n",
|
||
" <td>101,696,791</td>\n",
|
||
" <td>101,696,568</td>\n",
|
||
" <td>-0.00%</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>1</th>\n",
|
||
" <td>total_costs_pv</td>\n",
|
||
" <td>22,983,076</td>\n",
|
||
" <td>22,874,326</td>\n",
|
||
" <td>-0.47%</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>2</th>\n",
|
||
" <td>net_present_value</td>\n",
|
||
" <td>78,713,715</td>\n",
|
||
" <td>78,822,242</td>\n",
|
||
" <td>+0.14%</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>3</th>\n",
|
||
" <td>roi_percentage</td>\n",
|
||
" <td>342</td>\n",
|
||
" <td>345</td>\n",
|
||
" <td>+0.76%</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"</div>"
|
||
],
|
||
"text/plain": [
|
||
" metric published athena diff\n",
|
||
"0 total_benefits_pv 101,696,791 101,696,568 -0.00%\n",
|
||
"1 total_costs_pv 22,983,076 22,874,326 -0.47%\n",
|
||
"2 net_present_value 78,713,715 78,822,242 +0.14%\n",
|
||
"3 roi_percentage 342 345 +0.76%"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
},
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Payback: 1 months (published: <6 months)\n",
|
||
"✅ Verified — Athena reproduces the published Forrester totals.\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"# Published Forrester totals (3-yr risk-adjusted PV @ 10%)\n",
|
||
"PUBLISHED = {\n",
|
||
" \"total_benefits_pv\": 101_696_791,\n",
|
||
" \"total_costs_pv\": 22_983_076,\n",
|
||
" \"net_present_value\": 78_713_715,\n",
|
||
" \"roi_percentage\": 342,\n",
|
||
"}\n",
|
||
"# Tolerance: Athena discounts Year-0 'initial' amounts as Year 1 (Forrester\n",
|
||
"# leaves Year 0 undiscounted) — expected drift is ~$0.1M on costs (≈0.15%).\n",
|
||
"TOLERANCE = 0.02\n",
|
||
"\n",
|
||
"rows, ok = [], True\n",
|
||
"for key, expected in PUBLISHED.items():\n",
|
||
" actual = float(summary.get(key) or 0)\n",
|
||
" diff = (actual - expected) / expected\n",
|
||
" rows.append({\"metric\": key, \"published\": f\"{expected:,.0f}\",\n",
|
||
" \"athena\": f\"{actual:,.0f}\", \"diff\": f\"{diff:+.2%}\"})\n",
|
||
" ok &= abs(diff) <= TOLERANCE\n",
|
||
"\n",
|
||
"display(pd.DataFrame(rows))\n",
|
||
"payback = summary.get(\"payback_period_months\")\n",
|
||
"print(f\"Payback: {payback} months (published: <6 months)\")\n",
|
||
"assert ok, f\"Server totals drifted more than {TOLERANCE:.0%} from the published study — investigate before proceeding.\"\n",
|
||
"print(\"✅ Verified — Athena reproduces the published Forrester totals.\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "b48b6131",
|
||
"metadata": {},
|
||
"source": [
|
||
"## 7 · Save a baseline version & persist IDs"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 14,
|
||
"id": "148bdb2a",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Saved version 1 (baseline).\n",
|
||
"Saved to /Users/robert/git/palladium/.env:\n",
|
||
" PALLADIUM_REPORT_PUBLIC_ID=xsUTbjh4iDnJ\n",
|
||
" PALLADIUM_TOOL_PUBLIC_ID=pkrsQ9SRf654\n",
|
||
" PALLADIUM_PROPOSAL_ID=1\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"if not client.list_versions(TOOL_ID):\n",
|
||
" client.save_version(TOOL_ID, note=\"Baseline — published Forrester TEI figures (Feb 2026), moderate scenario.\")\n",
|
||
" print(\"Saved version 1 (baseline).\")\n",
|
||
"\n",
|
||
"ids = {\n",
|
||
" \"PALLADIUM_REPORT_PUBLIC_ID\": REPORT_ID,\n",
|
||
" \"PALLADIUM_TOOL_PUBLIC_ID\": TOOL_ID,\n",
|
||
"}\n",
|
||
"if PROPOSAL_ID is not None:\n",
|
||
" ids[\"PALLADIUM_PROPOSAL_ID\"] = str(PROPOSAL_ID)\n",
|
||
"if ENGAGEMENT_ID is not None:\n",
|
||
" ids[\"PALLADIUM_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}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "571b48c3",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Done\n",
|
||
"\n",
|
||
"The sandbox now has a live, calculated Amazon Connect TEI tool attached to\n",
|
||
"the selected client's proposal/engagement — with the client's CRM profile\n",
|
||
"(industry, agent counts, revenue) flowing into the tool automatically.\n",
|
||
"\n",
|
||
"- **Continue the analysis** → `01_benefits.ipynb` → `04_export.ipynb` (they pick up the IDs from `.env` via `config.py`)\n",
|
||
"- **Interactive editing** → `make app` / `streamlit run app/main.py` — the tool appears in the sidebar\n",
|
||
"- **CLI sanity check** → `python -m palladium summary $PALLADIUM_TOOL_PUBLIC_ID`"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "e9285087-5a2d-4a8d-856c-802474432892",
|
||
"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
|
||
}
|