"""Generate notebooks/diagnostic.ipynb from source cell text. The diagnostic has ~50 Mercury widgets (engagement form, six baseline inputs with confidence flags, 12 score sliders + 12 evidence fields, an export button). Hand-maintaining that JSON is error-prone, so the notebook is *generated* — cell sources live here as readable Python strings and nbformat writes valid JSON. Re-run after editing any cell: python scripts/build_notebook.py Then execute + export as usual (nbconvert / scripts/export_report.py). This is a build tool, not the engine — all study logic stays in diaglib. """ from __future__ import annotations import pathlib import nbformat as nbf ROOT = pathlib.Path(__file__).resolve().parent.parent OUT = ROOT / "notebooks" / "diagnostic.ipynb" # ── Cell sources ───────────────────────────────────────────────────── MD_TITLE = """\ # CX AI Advisory Diagnostic The facilitator's cockpit for the CX AI Advisory diagnostic workshop: capture capability scores across **12 competencies** in four dimensions, ingest the client's operational baseline, and watch the **value-at-stake analysis** — bounded by the capability gaps — recompute live in the room. **This notebook is the deliverable.** Serve it with `mercury --working-dir .` from the study root and share the screen. Work the sidebar top-to-bottom: engagement, baseline, then one competency at a time. The stage shows the current competency's card, the heatmap, the value analysis, and the unlock sequence. Click **Export JSON + CSV** at the bottom to write the structured engagement record to `exports/`. All content and math live in `diaglib/` and `configs/*.yaml` — the notebook only arranges and renders them. Outputs are **ranges, never point estimates**, and money displays at two significant figures. Confidence legend: 🟢 known · 🟡 estimated · 🔴 unknown — flag each baseline input; unknowns surface as explicit warnings in the analysis.""" SETUP = '''\ # ── Setup ────────────────────────────────────────────────────────── import sys, pathlib _ROOT = pathlib.Path.cwd() if not (_ROOT / "diaglib").exists(): # notebook lives in notebooks/ _ROOT = _ROOT.parent sys.path.insert(0, str(_ROOT)) import datetime as dt import html as _html import mercury as mr import pandas as pd from IPython.display import display # Single source of truth — all math and content live in the library; # only presentation (and Mercury widgets) lives here. from diaglib import ( BASELINE_FIELDS, CONFIDENCE_ICON, CSV_COLUMNS, OperationalBaseline, backstage, build_engagement, build_scores, configs_dir, dimension_rollup, engagement_json, evidence_coverage, heatmap_fig, heatmap_grid, list_industries, load_config, money, parse_participants, scores_dataframe, split_fig, unlock_fig, value_at_stake, value_bands_fig, write_exports, ) pd.options.display.float_format = "{:,.0f}".format CONFIGS_DIR = configs_dir(_ROOT) EXPORTS_DIR = _ROOT / "exports" INDUSTRIES = list_industries(CONFIGS_DIR) # The competency model is industry-independent (overlays may not redefine # it — the loader enforces that), so widgets can build from any config. _BASE = load_config("contact_center", CONFIGS_DIR) COMPETENCIES = _BASE.competencies DIMENSION_NAME = {d.id: d.name for d in _BASE.dimensions} # ── Brand palette (docs/brand.md, light theme) ───────────────────── NAVY, INK, MUTED = "#151d2c", "#2e404d", "#586671" BLUE, GREEN, LINE = "#0072bc", "#00a34c", "#e2e6e9" CARD_BG, HAIRLINE, HILITE = "#f8f8f8", "#d5d9db", "#dcecfa" FONT = "Georgia, 'Times New Roman', serif" BODY_FONT = "Arial, 'Helvetica Neue', Helvetica, sans-serif" def esc(s): return _html.escape(str(s)) # ── Workshop seeds — the gate's mock scenario; overwrite live ────── # Headless nbconvert renders every widget at its seed, so the seeds form # a coherent, gate-passing scenario (Pattern §3). SEED_CLIENT = "Acme Demo Co" SEED_DATE = "2026-07-19" SEED_FACILITATOR = "Robert Helewka" SEED_PARTICIPANTS = ("Jane Example | VP Customer Experience | cx; " "Sam Sample | Contact Center Ops Director | ops") SEED_BASELINE = { "annual_contact_volume": 1_200_000, "blended_cost_per_contact": 6.50, "agent_headcount": 450, "annual_attrition_rate": 0.30, "current_containment_rate": 0.20, "average_handle_time_seconds": 420, } CONFIDENCE_CHOICES = ["🟢 known", "🟡 estimated", "🔴 unknown"] SEED_CONFIDENCE = { "annual_contact_volume": "🟢 known", "blended_cost_per_contact": "🟡 estimated", "agent_headcount": "🟢 known", "annual_attrition_rate": "🟡 estimated", "current_containment_rate": "🟡 estimated", "average_handle_time_seconds": "🟢 known", } # Seed capability profile: data_readiness is the unique weakest # foundation, so the demo shows a single binding constraint. SEED_SCORES = { "automation_ai_strategy": (2, "AI driven by board pressure; no written thesis"), "value_realization": (2, "Business cases pre-investment only"), "executive_alignment": (3, "COO owns CX AI; steering meets quarterly"), "process_discovery": (3, "Top 10 call reasons mapped with volumes"), "data_readiness": (2, "KB stale; interaction data siloed in recordings"), "technical_architecture": (3, "CCaaS APIs available; shared integration layer WIP"), "use_case_prioritization": (3, "Scored backlog reviewed monthly"), "delivery_capability": (3, "Two bots in production via SI partner"), "talent_and_skills": (2, "One conversation designer, contractor"), "ai_operations": (2, "Containment eyeballed weekly, no drift alerts"), "change_adoption": (3, "Agent champions for copilot rollout"), "governance_and_risk": (3, "AI policy signed; review board for voice bots"), } # label, min, max, step per baseline field (explicit min/max — Pattern §3). BASELINE_META = { "annual_contact_volume": ("Annual contact volume", 0, 100_000_000, 10_000), "blended_cost_per_contact": ("Blended cost per contact ($)", 0, 100, 0.25), "agent_headcount": ("Agent headcount", 0, 100_000, 10), "annual_attrition_rate": ("Annual attrition rate (0-1)", 0, 1, 0.01), "current_containment_rate": ("Current containment rate (0-1)", 0, 1, 0.01), "average_handle_time_seconds": ("Average handle time (seconds)", 0, 3600, 10), } backstage(f"diaglib loaded — {len(COMPETENCIES)} competencies · " f"industries: {', '.join(INDUSTRIES)}")''' MD_HOWTO = """\ ## How to run this session - **Sidebar §1 — Engagement.** Client, industry config, date, participants (`Name | Role | Function` separated by `;` — functions: cx, it, ops, finance, other), and a running notes box. - **Sidebar §2 — Operational baseline.** Six numbers. If unknown, best estimate is fine — set the confidence flag and the analysis will carry the uncertainty explicitly. - **Sidebar §3 — Capability scoring.** Pick **Now scoring**, read the competency card on stage with the room, set the 1–5 slider, capture one line of evidence, move to the next. The heatmap and value analysis update live as you go. - **Export.** The button at the bottom of the page writes `exports/{engagement_id}.json` (source of truth) and `.csv` (flat scores) — a deliberate snapshot at click time. - **Backstage** (JupyterLab / nbconvert) — the verification gate and the machine-readable appendix; neither shows on the Mercury stage.""" W_ENGAGEMENT = '''\ # ── 1 · Engagement setup (sidebar — widgets ONLY, no other output) ── # Mercury re-runs only cells BELOW a changed widget: every .value is # read in the state cell further down, never here (Pattern §3). mr.Markdown("#### 1 · Engagement", position="sidebar") _client_w = mr.TextInput(label="Client name", value=SEED_CLIENT) _industry_w = mr.Select(label="Industry config", value="contact_center", choices=INDUSTRIES) _date_w = mr.DateInput(label="Workshop date", value=SEED_DATE) _facilitator_w = mr.TextInput(label="Facilitator", value=SEED_FACILITATOR) _participants_w = mr.TextInput( label="Participants — Name | Role | Function; ...", value=SEED_PARTICIPANTS) _notes_w = mr.TextInput(label="Session notes", value="")''' W_BASELINE = '''\ # ── 2 · Operational baseline (sidebar — widgets ONLY) ─────────────── # Six numbers + a confidence flag each. Explicit min/max on every # NumberInput — Mercury clamps out-of-range seeds to a default range. mr.Markdown("#### 2 · Operational baseline", position="sidebar") _baseline_w, _conf_w = {}, {} for _f in BASELINE_FIELDS: _label, _min, _max, _step = BASELINE_META[_f] _baseline_w[_f] = mr.NumberInput(label=_label, value=SEED_BASELINE[_f], min=_min, max=_max, step=_step) _conf_w[_f] = mr.Select(label=f"{_label} — confidence", value=SEED_CONFIDENCE[_f], choices=CONFIDENCE_CHOICES)''' W_SCORING = '''\ # ── 3 · Capability scoring (sidebar — widgets ONLY) ───────────────── # One screen per competency on stage: pick "Now scoring", read the card # with the room, set the slider, capture one line of evidence. Labels # are distinct per widget so Mercury's cache never collides. mr.Markdown("#### 3 · Capability scoring", position="sidebar") _now_scoring_w = mr.Select( label="Now scoring", value=f"1 · {COMPETENCIES[0].name}", choices=[f"{_i + 1} · {_c.name}" for _i, _c in enumerate(COMPETENCIES)]) _score_w, _evidence_w = {}, {} _prev_dim = None for _c in COMPETENCIES: if _c.dimension != _prev_dim: mr.Markdown(f"**{DIMENSION_NAME[_c.dimension]}**", position="sidebar") _prev_dim = _c.dimension _score_w[_c.id] = mr.Slider(label=f"Score — {_c.name}", min=1, max=5, value=SEED_SCORES[_c.id][0]) _evidence_w[_c.id] = mr.TextInput(label=f"Evidence — {_c.name}", value=SEED_SCORES[_c.id][1])''' STATE = '''\ # ── Session state (re-runs on any sidebar change) ─────────────────── # Read every widget .value; all computation happens in diaglib. CLIENT_NAME = str(_client_w.value).strip() or SEED_CLIENT INDUSTRY = str(_industry_w.value) CONFIG = load_config(INDUSTRY, CONFIGS_DIR) WORKSHOP_DATE = dt.date.fromisoformat(str(_date_w.value) or SEED_DATE) PARTICIPANTS = parse_participants(str(_participants_w.value)) NOTES = str(_notes_w.value) _vals = {f: float(_baseline_w[f].value) for f in BASELINE_FIELDS} BASELINE = OperationalBaseline( annual_contact_volume=int(_vals["annual_contact_volume"]), blended_cost_per_contact=_vals["blended_cost_per_contact"], agent_headcount=int(_vals["agent_headcount"]), annual_attrition_rate=_vals["annual_attrition_rate"], current_containment_rate=_vals["current_containment_rate"], average_handle_time_seconds=int(_vals["average_handle_time_seconds"]), field_confidence={f: str(_conf_w[f].value).split()[-1] for f in BASELINE_FIELDS}, ) RAW_SCORES = {c.id: (int(_score_w[c.id].value), str(_evidence_w[c.id].value)) for c in COMPETENCIES} SCORES = build_scores(CONFIG, RAW_SCORES, scored_at=dt.datetime.now()) VAS = value_at_stake(CONFIG, BASELINE, SCORES) ENGAGEMENT = build_engagement( config=CONFIG, client_name=CLIENT_NAME, facilitator=str(_facilitator_w.value), workshop_date=WORKSHOP_DATE, participants=PARTICIPANTS, baseline=BASELINE, scores=SCORES, computed_value=VAS, notes=NOTES) NOW_SCORING = COMPETENCIES[int(str(_now_scoring_w.value).split(" · ")[0]) - 1] _done, _total = evidence_coverage(SCORES) # One curated line on stage; warnings surface for the room; echo backstage. if CONFIG.value_drivers: _bind = ", ".join(CONFIG.competency(c).name for c in VAS.binding_constraints) print(f"Value at stake: {money(VAS.theoretical_annual_value_low)}-" f"{money(VAS.theoretical_annual_value_high)} theoretical per year · " f"{money(VAS.realizable_18mo_low)}-{money(VAS.realizable_18mo_high)} " f"realizable over 18 months — capped at level " f"{VAS.weakest_foundational_score} by {_bind}") for _warning in VAS.warnings: print(f"⚠ {_warning}") backstage(f"engagement {ENGAGEMENT.engagement_id} · " f"{len(PARTICIPANTS)} participants · evidence {_done}/{_total}")''' STAGE_HEADER = '''\ # ── Stage: engagement banner + baseline echo ──────────────────────── _chips = "".join( f'{esc(p.name)}' + (f" · {esc(p.role)}" if p.role else "") + f' {esc(p.function)}' for p in PARTICIPANTS) or ( f'' f'no participants captured yet') _rows = "" for _f in BASELINE_FIELDS: _v = getattr(BASELINE, _f) _shown = f"{_v:,.2f}" if isinstance(_v, float) and _v < 10 else f"{_v:,.0f}" _rows += ( f'
configs/{esc(INDUSTRY)}.yaml.