Files
Robert Helewka ecd164ee6d feat: add locale formatting config and update notebook outputs
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.
2026-06-10 11:54:28 -04:00

1005 lines
34 KiB
Plaintext
Raw Permalink 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.
{
"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
}