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.
This commit is contained in:
2026-06-10 11:54:28 -04:00
parent 253ff38118
commit ecd164ee6d
13 changed files with 839 additions and 111 deletions

View File

@@ -21,7 +21,7 @@
},
{
"cell_type": "code",
"execution_count": 5,
"execution_count": 2,
"id": "5bcc7740",
"metadata": {},
"outputs": [
@@ -58,7 +58,7 @@
},
{
"cell_type": "code",
"execution_count": 6,
"execution_count": 3,
"id": "386ae38b",
"metadata": {},
"outputs": [
@@ -111,7 +111,7 @@
},
{
"cell_type": "code",
"execution_count": 7,
"execution_count": 4,
"id": "dc46ab46",
"metadata": {},
"outputs": [
@@ -198,20 +198,89 @@
},
{
"cell_type": "code",
"execution_count": 12,
"execution_count": 5,
"id": "4070b9c2",
"metadata": {},
"outputs": [
{
"ename": "AttributeError",
"evalue": "'TEIClient' object has no attribute 'list_clients'",
"output_type": "error",
"traceback": [
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
"\u001b[31mAttributeError\u001b[39m Traceback (most recent call last)",
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[12]\u001b[39m\u001b[32m, line 3\u001b[39m\n\u001b[32m 1\u001b[39m CLIENT_SEARCH = \u001b[33m\"Global\"\u001b[39m \u001b[38;5;66;03m# e.g. \"Acme\" — empty lists everyone\u001b[39;00m\n\u001b[32m 2\u001b[39m \n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m clients = client.list_clients(search=CLIENT_SEARCH \u001b[38;5;28;01mor\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m)\n\u001b[32m 4\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m clients:\n\u001b[32m 5\u001b[39m display(pd.DataFrame(clients)[\n\u001b[32m 6\u001b[39m [c for c in (\"id\", \"name\", \"vertical\", \"client_type\", \"employee_count\",\n",
"\u001b[31mAttributeError\u001b[39m: 'TEIClient' object has no attribute 'list_clients'"
]
"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": [
@@ -230,24 +299,112 @@
},
{
"cell_type": "code",
"execution_count": 10,
"execution_count": 6,
"id": "4e97978c",
"metadata": {},
"outputs": [
{
"ename": "NameError",
"evalue": "name 'clients' is not defined",
"output_type": "error",
"traceback": [
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
"\u001b[31mNameError\u001b[39m Traceback (most recent call last)",
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[10]\u001b[39m\u001b[32m, line 3\u001b[39m\n\u001b[32m 1\u001b[39m CLIENT_ID = \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;66;03m# ← set from the `id` column above, or leave for auto-pick\u001b[39;00m\n\u001b[32m 2\u001b[39m \n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m CLIENT_ID \u001b[38;5;28;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;28;01mand\u001b[39;00m len(clients) == \u001b[32m1\u001b[39m:\n\u001b[32m 4\u001b[39m CLIENT_ID = clients[\u001b[32m0\u001b[39m][\u001b[33m\"id\"\u001b[39m]\n\u001b[32m 5\u001b[39m print(f\"Auto-selected the only client: {clients[\u001b[32m0\u001b[39m][\u001b[33m'name'\u001b[39m]} (id={CLIENT_ID})\")\n\u001b[32m 6\u001b[39m \u001b[38;5;28;01massert\u001b[39;00m CLIENT_ID \u001b[38;5;28;01mis\u001b[39;00m \u001b[38;5;28;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m, \u001b[33m\"Set CLIENT_ID from the table above and re-run this cell.\"\u001b[39m\n",
"\u001b[31mNameError\u001b[39m: name 'clients' is not defined"
"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 = None # ← set from the `id` column above, or leave for auto-pick\n",
"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",
@@ -274,19 +431,60 @@
},
{
"cell_type": "code",
"execution_count": 11,
"execution_count": 7,
"id": "fcccc591",
"metadata": {},
"outputs": [
{
"ename": "NameError",
"evalue": "name 'profile' is not defined",
"output_type": "error",
"traceback": [
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
"\u001b[31mNameError\u001b[39m Traceback (most recent call last)",
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[11]\u001b[39m\u001b[32m, line 3\u001b[39m\n\u001b[32m 1\u001b[39m CLIENT_ASSUMPTIONS = dict(seed.ASSUMPTIONS)\n\u001b[32m 2\u001b[39m overrides = {\n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m \u001b[33m\"agents_fte\"\u001b[39m: profile.get(\u001b[33m\"contact_center_agent_count\"\u001b[39m),\n\u001b[32m 4\u001b[39m \u001b[33m\"supervisors_fte\"\u001b[39m: profile.get(\u001b[33m\"supervisor_count\"\u001b[39m),\n\u001b[32m 5\u001b[39m }\n\u001b[32m 6\u001b[39m rows = []\n",
"\u001b[31mNameError\u001b[39m: name 'profile' is not defined"
"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"
]
}
],
@@ -326,10 +524,70 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 8,
"id": "57dec6cf",
"metadata": {},
"outputs": [],
"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",
@@ -356,10 +614,19 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 9,
"id": "19336bcc",
"metadata": {},
"outputs": [],
"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",
@@ -406,10 +673,18 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 10,
"id": "017ae9db",
"metadata": {},
"outputs": [],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Created tool pkrsQ9SRf654 attached to {'proposal': 1}\n"
]
}
],
"source": [
"from core.tei_client import AthenaAPIError\n",
"\n",
@@ -452,10 +727,18 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 11,
"id": "20e2a736",
"metadata": {},
"outputs": [],
"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",
@@ -487,10 +770,27 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 12,
"id": "b7ac5d24",
"metadata": {},
"outputs": [],
"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)"
@@ -498,10 +798,90 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 13,
"id": "13d84001",
"metadata": {},
"outputs": [],
"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",
@@ -539,10 +919,22 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 14,
"id": "148bdb2a",
"metadata": {},
"outputs": [],
"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",
@@ -578,6 +970,14 @@
"- **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": {