CX Discovery Notebook
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
discoverylib — self-contained engine for the CX Exploration & Discovery
|
||||
Workshop (Mercury Notebook Pattern).
|
||||
|
||||
Not a financial model: this study's deliverable is a **live facilitation
|
||||
aid**. The Mercury stage is the visual the client watches on the call —
|
||||
topic board, the active topic's live sub-topic checklist, and a progress
|
||||
headline ("3/8 topics complete"). The backstage (JupyterLab / nbconvert)
|
||||
carries the facilitator's question script and the captured per-topic notes,
|
||||
which the data-appendix export hands to an LLM to draft the survey write-up
|
||||
or feed a downstream business-case study.
|
||||
|
||||
Engine/presentation split (Pattern §2): the topic bank and all status /
|
||||
progress logic live here; the notebook only arranges and renders them.
|
||||
"""
|
||||
|
||||
from .topics import (
|
||||
TOPIC_BY_KEY,
|
||||
TOPIC_KEYS,
|
||||
TOPICS,
|
||||
SubTopic,
|
||||
Topic,
|
||||
topic,
|
||||
)
|
||||
from .session import (
|
||||
COMPLETE,
|
||||
IN_PROGRESS,
|
||||
NOT_STARTED,
|
||||
SKIPPED,
|
||||
STATUS_COLOR,
|
||||
STATUS_GLYPH,
|
||||
STATUS_LABEL,
|
||||
STATUSES,
|
||||
ChecklistItem,
|
||||
Progress,
|
||||
TopicState,
|
||||
active_topic_key,
|
||||
agenda_minutes,
|
||||
build_session,
|
||||
normalize_status,
|
||||
progress,
|
||||
session_json,
|
||||
subtopic_checklist,
|
||||
subtopic_id,
|
||||
)
|
||||
from .staging import backstage, on_stage
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
||||
__all__ = [
|
||||
# topic bank
|
||||
"TOPICS", "TOPIC_BY_KEY", "TOPIC_KEYS", "Topic", "SubTopic", "topic",
|
||||
# status vocabulary
|
||||
"STATUSES", "STATUS_LABEL", "STATUS_GLYPH", "STATUS_COLOR",
|
||||
"NOT_STARTED", "IN_PROGRESS", "COMPLETE", "SKIPPED", "normalize_status",
|
||||
# progress + session
|
||||
"Progress", "progress", "active_topic_key", "agenda_minutes",
|
||||
"ChecklistItem", "subtopic_checklist", "subtopic_id",
|
||||
"TopicState", "build_session", "session_json",
|
||||
# staging
|
||||
"on_stage", "backstage",
|
||||
]
|
||||
251
studies/202607_CX_Discovery_Workshop/discoverylib/session.py
Normal file
251
studies/202607_CX_Discovery_Workshop/discoverylib/session.py
Normal file
@@ -0,0 +1,251 @@
|
||||
"""
|
||||
Session engine — the discovery workshop's "math".
|
||||
|
||||
There are no dollars here; the quantities a facilitator and client watch are
|
||||
**status** and **progress**. This module owns all of it so the notebook only
|
||||
arranges outputs (Mercury Notebook Pattern §2, engine/presentation split):
|
||||
|
||||
* the topic-status vocabulary (:data:`STATUSES`) and its display glyphs,
|
||||
* :func:`progress` — the "3 / 8 topics complete" headline and its ratios,
|
||||
* :func:`subtopic_checklist` — the live tick-list for the active topic,
|
||||
* :func:`build_session` — assembles the full render/export state from the
|
||||
raw widget inputs (per-topic status + free-text notes),
|
||||
* :func:`session_json` — the machine-readable export payload (Pattern §5).
|
||||
|
||||
Everything is a pure function of ``(status_by_topic, notes_by_topic,
|
||||
done_subtopics)`` so the in-notebook gate can pin it and the export can dump
|
||||
it. Unknown / missing keys degrade gracefully to "not started" so a
|
||||
half-filled live session never crashes the render.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from .topics import TOPICS, TOPIC_BY_KEY, Topic
|
||||
|
||||
|
||||
# ── Status vocabulary ────────────────────────────────────────────────
|
||||
# Order matters: it's the selector order and the legend order. "skipped"
|
||||
# is terminal-but-not-complete (a topic consciously set aside), so it
|
||||
# counts as resolved for the agenda but not toward completion.
|
||||
NOT_STARTED = "not_started"
|
||||
IN_PROGRESS = "in_progress"
|
||||
COMPLETE = "complete"
|
||||
SKIPPED = "skipped"
|
||||
|
||||
STATUSES: tuple[str, ...] = (NOT_STARTED, IN_PROGRESS, COMPLETE, SKIPPED)
|
||||
|
||||
STATUS_LABEL: dict[str, str] = {
|
||||
NOT_STARTED: "Not started",
|
||||
IN_PROGRESS: "In progress",
|
||||
COMPLETE: "Complete",
|
||||
SKIPPED: "Skipped",
|
||||
}
|
||||
|
||||
# Stage glyphs — calm, unambiguous at a glance on a shared screen.
|
||||
STATUS_GLYPH: dict[str, str] = {
|
||||
NOT_STARTED: "○", # open circle — pending
|
||||
IN_PROGRESS: "◐", # half — under discussion
|
||||
COMPLETE: "●", # filled — done
|
||||
SKIPPED: "⊘", # slashed — set aside
|
||||
}
|
||||
|
||||
# Brand-aligned status colors (docs/brand.md). Muted gray pending, Future
|
||||
# Blue active, Success Green complete, light gray skipped.
|
||||
STATUS_COLOR: dict[str, str] = {
|
||||
NOT_STARTED: "#a9b2b8",
|
||||
IN_PROGRESS: "#0072bc",
|
||||
COMPLETE: "#00a34c",
|
||||
SKIPPED: "#c3c7ca",
|
||||
}
|
||||
|
||||
|
||||
def normalize_status(value: str | None) -> str:
|
||||
"""Coerce any widget value to a known status; default NOT_STARTED."""
|
||||
return value if value in STATUS_LABEL else NOT_STARTED
|
||||
|
||||
|
||||
# ── Progress ─────────────────────────────────────────────────────────
|
||||
@dataclass(frozen=True)
|
||||
class Progress:
|
||||
"""The headline the client watches: completed vs. total topics, plus
|
||||
the resolved (complete + skipped) count that drives the agenda burn."""
|
||||
|
||||
total: int
|
||||
completed: int # status == complete
|
||||
skipped: int
|
||||
in_progress: int
|
||||
not_started: int
|
||||
|
||||
@property
|
||||
def resolved(self) -> int:
|
||||
"""Topics no longer open for discussion (complete or skipped)."""
|
||||
return self.completed + self.skipped
|
||||
|
||||
@property
|
||||
def fraction(self) -> float:
|
||||
"""Completed / total in [0, 1] — the progress-bar fill."""
|
||||
return self.completed / self.total if self.total else 0.0
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
return f"{self.completed}/{self.total} topics complete"
|
||||
|
||||
|
||||
def progress(status_by_topic: dict[str, str]) -> Progress:
|
||||
"""Tally topic statuses into the headline :class:`Progress`."""
|
||||
counts = {s: 0 for s in STATUSES}
|
||||
for t in TOPICS:
|
||||
counts[normalize_status(status_by_topic.get(t.key))] += 1
|
||||
return Progress(
|
||||
total=len(TOPICS),
|
||||
completed=counts[COMPLETE],
|
||||
skipped=counts[SKIPPED],
|
||||
in_progress=counts[IN_PROGRESS],
|
||||
not_started=counts[NOT_STARTED],
|
||||
)
|
||||
|
||||
|
||||
def active_topic_key(status_by_topic: dict[str, str]) -> str | None:
|
||||
"""The topic to spotlight on stage: the first in-progress topic, else
|
||||
the first not-started one, else None (everything resolved)."""
|
||||
for t in TOPICS:
|
||||
if normalize_status(status_by_topic.get(t.key)) == IN_PROGRESS:
|
||||
return t.key
|
||||
for t in TOPICS:
|
||||
if normalize_status(status_by_topic.get(t.key)) == NOT_STARTED:
|
||||
return t.key
|
||||
return None
|
||||
|
||||
|
||||
# ── Sub-topic checklist (live, for the active topic) ─────────────────
|
||||
@dataclass(frozen=True)
|
||||
class ChecklistItem:
|
||||
key: str
|
||||
title: str
|
||||
done: bool
|
||||
|
||||
|
||||
def subtopic_checklist(
|
||||
topic: Topic, done_subtopics: set[str] | frozenset[str]
|
||||
) -> list[ChecklistItem]:
|
||||
"""The active topic's sub-topics as a tick-list. ``done_subtopics``
|
||||
holds the fully-qualified ``"<topic_key>.<subtopic_key>"`` ids the
|
||||
facilitator has ticked."""
|
||||
return [
|
||||
ChecklistItem(st.key, st.title, f"{topic.key}.{st.key}" in done_subtopics)
|
||||
for st in topic.subtopics
|
||||
]
|
||||
|
||||
|
||||
def subtopic_id(topic_key: str, subtopic_key: str) -> str:
|
||||
"""The fully-qualified id used in ``done_subtopics`` and the export."""
|
||||
return f"{topic_key}.{subtopic_key}"
|
||||
|
||||
|
||||
# ── Agenda ───────────────────────────────────────────────────────────
|
||||
def agenda_minutes() -> int:
|
||||
"""Sum of the nominal per-topic minute budgets."""
|
||||
return sum(t.minutes for t in TOPICS)
|
||||
|
||||
|
||||
# ── Full session state (render + export) ─────────────────────────────
|
||||
@dataclass(frozen=True)
|
||||
class TopicState:
|
||||
key: str
|
||||
title: str
|
||||
scope: str
|
||||
minutes: int
|
||||
status: str
|
||||
status_label: str
|
||||
glyph: str
|
||||
color: str
|
||||
notes: str
|
||||
subtopics_total: int
|
||||
subtopics_done: int
|
||||
|
||||
|
||||
def build_session(
|
||||
status_by_topic: dict[str, str],
|
||||
notes_by_topic: dict[str, str] | None = None,
|
||||
done_subtopics: set[str] | frozenset[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Assemble the complete session state the notebook renders and exports.
|
||||
|
||||
Pure function of the three raw inputs; safe against missing keys.
|
||||
"""
|
||||
notes_by_topic = notes_by_topic or {}
|
||||
done_subtopics = frozenset(done_subtopics or ())
|
||||
|
||||
topic_states: list[TopicState] = []
|
||||
for t in TOPICS:
|
||||
status = normalize_status(status_by_topic.get(t.key))
|
||||
done = sum(
|
||||
1 for st in t.subtopics if subtopic_id(t.key, st.key) in done_subtopics
|
||||
)
|
||||
topic_states.append(
|
||||
TopicState(
|
||||
key=t.key,
|
||||
title=t.title,
|
||||
scope=t.scope,
|
||||
minutes=t.minutes,
|
||||
status=status,
|
||||
status_label=STATUS_LABEL[status],
|
||||
glyph=STATUS_GLYPH[status],
|
||||
color=STATUS_COLOR[status],
|
||||
notes=(notes_by_topic.get(t.key) or "").strip(),
|
||||
subtopics_total=len(t.subtopics),
|
||||
subtopics_done=done,
|
||||
)
|
||||
)
|
||||
|
||||
prog = progress(status_by_topic)
|
||||
active = active_topic_key(status_by_topic)
|
||||
return {
|
||||
"topics": topic_states,
|
||||
"progress": prog,
|
||||
"active_topic_key": active,
|
||||
"active_topic": TOPIC_BY_KEY[active] if active else None,
|
||||
"agenda_minutes": agenda_minutes(),
|
||||
}
|
||||
|
||||
|
||||
def session_json(
|
||||
session: dict[str, Any], meta: dict[str, Any] | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""The machine-readable export payload (Pattern §5): every captured
|
||||
answer/status/note as plain JSON, ready to feed an LLM drafting the
|
||||
survey write-up or business case. Plotly/HTML never carry this — the
|
||||
appendix does."""
|
||||
prog: Progress = session["progress"]
|
||||
return {
|
||||
"study": "202607_CX_Discovery_Workshop",
|
||||
"instrument": "CX Exploration & Discovery Workshop",
|
||||
"meta": meta or {},
|
||||
"progress": {
|
||||
"total_topics": prog.total,
|
||||
"completed": prog.completed,
|
||||
"skipped": prog.skipped,
|
||||
"in_progress": prog.in_progress,
|
||||
"not_started": prog.not_started,
|
||||
"fraction_complete": round(prog.fraction, 4),
|
||||
"label": prog.label,
|
||||
},
|
||||
"agenda_minutes": session["agenda_minutes"],
|
||||
"active_topic": session["active_topic_key"],
|
||||
"topics": [
|
||||
{
|
||||
"key": ts.key,
|
||||
"title": ts.title,
|
||||
"scope": ts.scope,
|
||||
"status": ts.status,
|
||||
"minutes": ts.minutes,
|
||||
"subtopics_done": ts.subtopics_done,
|
||||
"subtopics_total": ts.subtopics_total,
|
||||
"notes": ts.notes,
|
||||
}
|
||||
for ts in session["topics"]
|
||||
],
|
||||
}
|
||||
29
studies/202607_CX_Discovery_Workshop/discoverylib/staging.py
Normal file
29
studies/202607_CX_Discovery_Workshop/discoverylib/staging.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""
|
||||
Stage vs backstage — is this notebook render stakeholder-facing?
|
||||
|
||||
The Mercury CLI (``mercury --working-dir …``) exports ``MERCURY_CONFIG_DIR``
|
||||
into the server process so the widget library can locate ``config.toml``
|
||||
(see ``mercury/config.py``); every kernel that server spawns inherits it.
|
||||
JupyterLab and nbconvert kernels don't have it. That makes the variable a
|
||||
reliable signal for "the audience is looking" (the stage) versus an
|
||||
analyst session or a headless export run (backstage).
|
||||
|
||||
Diagnostics routed through :func:`backstage` stay visible in JupyterLab
|
||||
and land in the nbconvert exports (where the machine-readable appendix
|
||||
must appear for LLM consumption) but never render in the Mercury app.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def on_stage() -> bool:
|
||||
"""True when running under the Mercury app (stakeholder-facing)."""
|
||||
return os.getenv("MERCURY_CONFIG_DIR") is not None
|
||||
|
||||
|
||||
def backstage(*args, **kwargs) -> None:
|
||||
"""``print`` that renders only backstage (JupyterLab, nbconvert)."""
|
||||
if not on_stage():
|
||||
print(*args, **kwargs)
|
||||
428
studies/202607_CX_Discovery_Workshop/discoverylib/topics.py
Normal file
428
studies/202607_CX_Discovery_Workshop/discoverylib/topics.py
Normal file
@@ -0,0 +1,428 @@
|
||||
"""
|
||||
The discovery topic bank — verbatim source record for the CX Exploration &
|
||||
Discovery workshop.
|
||||
|
||||
This is the study's ``*_VERBATIM`` anchor (Mercury Notebook Pattern): the
|
||||
facilitation content, structured but **never paraphrased away from** the
|
||||
source survey ``docs/cx_discovery_survey.md`` (the original ``cxxm.md``).
|
||||
Editing the wording of a prompt here is editing the anchor — do it against
|
||||
the survey, not the notebook.
|
||||
|
||||
Shape
|
||||
-----
|
||||
``TOPICS`` is an ordered tuple of :class:`Topic`. Each topic owns an ordered
|
||||
tuple of :class:`SubTopic`; each sub-topic owns the ordered facilitator
|
||||
prompts (the questions you actually ask). The stage shows topic titles, the
|
||||
one-line ``scope`` per topic, and — for the active topic — the sub-topic
|
||||
titles as a live checklist. The prompts stay **backstage** (your script);
|
||||
they never render on the client's screen.
|
||||
|
||||
Every topic carries:
|
||||
|
||||
* ``key`` — stable slug (snake_case); the identity used by widgets, notes,
|
||||
status, and the JSON export. NEVER renumber or rename casually —
|
||||
captured notes key off it.
|
||||
* ``title`` — client-facing heading shown on stage.
|
||||
* ``scope`` — one neutral line shown under the title on stage, so the client
|
||||
stays oriented without being led by the questions.
|
||||
* ``minutes`` — nominal facilitation budget, summed into the agenda estimate.
|
||||
|
||||
The content is deliberately data, not prose in the notebook: it is testable,
|
||||
diffable, and exportable, and the notebook only arranges it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SubTopic:
|
||||
"""A discussion thread within a topic; its title shows on stage when the
|
||||
topic is active, its prompts are the backstage facilitation script."""
|
||||
|
||||
key: str
|
||||
title: str
|
||||
prompts: tuple[str, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Topic:
|
||||
"""A client-facing discovery topic: the unit the progress bar counts."""
|
||||
|
||||
key: str
|
||||
title: str
|
||||
scope: str
|
||||
minutes: int
|
||||
subtopics: tuple[SubTopic, ...] = field(default_factory=tuple)
|
||||
|
||||
@property
|
||||
def prompt_count(self) -> int:
|
||||
return sum(len(st.prompts) for st in self.subtopics)
|
||||
|
||||
|
||||
# ── The topic bank ───────────────────────────────────────────────────
|
||||
# Ordered as a workshop runs: context first, then the operational areas,
|
||||
# then the enabling functions, closing on reporting & insight. Wording of
|
||||
# prompts tracks docs/cx_discovery_survey.md (cxxm.md).
|
||||
|
||||
TOPICS: tuple[Topic, ...] = (
|
||||
Topic(
|
||||
key="background",
|
||||
title="Background & Organization",
|
||||
scope="Lines of business, channels, org structure, decision-making",
|
||||
minutes=15,
|
||||
subtopics=(
|
||||
SubTopic(
|
||||
"lines_of_business", "Lines of business & scope",
|
||||
(
|
||||
"How many distinct lines of business are supported in the "
|
||||
"contact centre? (e.g. commercial, residential, retail, "
|
||||
"wholesale)",
|
||||
"Are you using the contact centre for internal uses such as "
|
||||
"help desk, finance, HR?",
|
||||
"Payment card / PCI in scope?",
|
||||
"What are the availability SLAs or targets for your "
|
||||
"technology platform?",
|
||||
),
|
||||
),
|
||||
SubTopic(
|
||||
"channels_supported", "Channels supported",
|
||||
(
|
||||
"Which channels do you support today? (Apps, Voice, Video, "
|
||||
"Chat, SMS, Email, Social Media, Digital Assistants)",
|
||||
),
|
||||
),
|
||||
SubTopic(
|
||||
"org_structure", "Org & reporting structure",
|
||||
(
|
||||
"Mini org chart — reporting structure up to the executive "
|
||||
"leader.",
|
||||
"Are your contact centres managed by the same person? "
|
||||
"(Managers / Team Leads / Agents)",
|
||||
"Who carries the cost of contact centre agents and "
|
||||
"supervisors?",
|
||||
"What revenue is generated by the contact centre?",
|
||||
),
|
||||
),
|
||||
SubTopic(
|
||||
"decision_making", "Decision-making & priorities",
|
||||
(
|
||||
"Is decision-making centralized or decentralized?",
|
||||
"Who are the key decision makers for changes to the contact "
|
||||
"centre?",
|
||||
"How important is CX & EX to your organization's strategy? "
|
||||
"Is there an executive accountable for CX (CXO, CDO)?",
|
||||
"Of your leading offers, what is the customer's top priority?",
|
||||
"Competitive pressures?",
|
||||
"Any pending or recent acquisitions or spinoffs affecting IT "
|
||||
"infrastructure and services?",
|
||||
),
|
||||
),
|
||||
SubTopic(
|
||||
"problems_today", "Problems & causes today",
|
||||
(
|
||||
"What do you view as the problems in your contact centre "
|
||||
"today, and the primary causes?",
|
||||
"Any in-flight projects — CX or EX improvement initiatives?",
|
||||
"Are your customer experiences personalized?",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Topic(
|
||||
key="cx_strategy",
|
||||
title="CX Strategy",
|
||||
scope="How CX value is defined, measured, and organized to deliver",
|
||||
minutes=15,
|
||||
subtopics=(
|
||||
SubTopic(
|
||||
"value_definition", "Value & strategic position",
|
||||
(
|
||||
"How is the value of Customer Experience defined within your "
|
||||
"organization? Is CX a key BPI, recognized & measured as a "
|
||||
"financial value?",
|
||||
"What are the key capabilities / services delivered by the "
|
||||
"contact centre?",
|
||||
"How are you using CX innovation to create market "
|
||||
"disruption?",
|
||||
"How are competitors using CX to create competitive "
|
||||
"differentiation?",
|
||||
),
|
||||
),
|
||||
SubTopic(
|
||||
"operating_model", "Organization & operating model",
|
||||
(
|
||||
"Is there a CX team? Do the CX insights team regularly "
|
||||
"educate the business?",
|
||||
"Who are the key decision makers for investment and changes "
|
||||
"to the contact centre? Channel management strategy?",
|
||||
"What are the teams, and how are processes managed?",
|
||||
"Describe your automation strategy. Who is your leader for "
|
||||
"Data & AI?",
|
||||
),
|
||||
),
|
||||
SubTopic(
|
||||
"insight_and_voc", "Insight & Voice of Customer",
|
||||
(
|
||||
"How do you use analytics & data to generate a consolidated "
|
||||
"view of your customer experience?",
|
||||
"Is there a VOC program in place? ROI / business value "
|
||||
"known?",
|
||||
"How do customers rate the experience they receive (0–5)?",
|
||||
),
|
||||
),
|
||||
SubTopic(
|
||||
"continuous_improvement", "Continuous improvement",
|
||||
(
|
||||
"How is customer insight used to drive CX improvement, "
|
||||
"loyalty and profitability? How do you anticipate needs?",
|
||||
"Do you have a clear set of CX design guidelines? (Personas, "
|
||||
"Journey Mapping, Tools)",
|
||||
"KPI targets for CX — CSAT, NPS, CES? How are you doing?",
|
||||
),
|
||||
),
|
||||
SubTopic(
|
||||
"employee_engagement", "Employee engagement",
|
||||
(
|
||||
"Describe your employee / agent engagement strategy.",
|
||||
"How engaged are your people in delivering the customer "
|
||||
"experience?",
|
||||
"Do you have a VoA / VoE program in place? How are you "
|
||||
"doing?",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Topic(
|
||||
key="channels",
|
||||
title="Channels",
|
||||
scope="Inbound & outbound volumes, metrics, contact reasons",
|
||||
minutes=20,
|
||||
subtopics=(
|
||||
SubTopic(
|
||||
"inbound_context", "Inbound context",
|
||||
(
|
||||
"Hours of operation — any 24×7?",
|
||||
"Who contacts the contact centre? (Demographics, their "
|
||||
"situation)",
|
||||
"Are certain callers or groups prioritized?",
|
||||
"Languages? (English, Canadian French, Spanish, other)",
|
||||
),
|
||||
),
|
||||
SubTopic(
|
||||
"voice_metrics", "Voice & video metrics",
|
||||
(
|
||||
"Average number of active agents (voice / video).",
|
||||
"Toll-free numbers & DIDs — approximate quantities.",
|
||||
"Average Wait Time, Abandon rate, Handle Time, Hold Time, "
|
||||
"After-call work time.",
|
||||
"Time to authenticate a caller.",
|
||||
"% of calls transferred — internally / externally (3rd "
|
||||
"parties)?",
|
||||
"Courtesy callback / virtual hold? Post-call survey? First "
|
||||
"Call Resolution rate? Average revenue per call (sales)?",
|
||||
),
|
||||
),
|
||||
SubTopic(
|
||||
"digital_channels", "Digital channels",
|
||||
(
|
||||
"Email — range of logged-in users, response time, email "
|
||||
"server.",
|
||||
"Chat — web (internal/external), app-embedded, SMS, "
|
||||
"Messenger, WhatsApp, Telegram, iMessage.",
|
||||
"Website forms, mobile apps.",
|
||||
),
|
||||
),
|
||||
SubTopic(
|
||||
"inbound_reasons", "Inbound contact reasons",
|
||||
(
|
||||
"Top 3–5 inbound contact reasons and approximate % of calls.",
|
||||
"Cost per call? Average Handle Time?",
|
||||
"Busiest / least busy days? Seasonal variances?",
|
||||
"Most difficult, commonly occurring calls? Easiest commonly "
|
||||
"occurring call?",
|
||||
),
|
||||
),
|
||||
SubTopic(
|
||||
"outbound", "Outbound",
|
||||
(
|
||||
"Hours of operation.",
|
||||
"Voice (preview / predictive dialer volume), email, SMS, "
|
||||
"recorded announcement ports.",
|
||||
"Self-service applications, live agent connect, campaign "
|
||||
"management, DNC management.",
|
||||
"Top 3–5 outbound contact reasons and approximate % of "
|
||||
"calls.",
|
||||
"After-call work?",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Topic(
|
||||
key="agent_environment",
|
||||
title="Agent & Supervisor Environment",
|
||||
scope="Locations, endpoints, desktop, applications, knowledge",
|
||||
minutes=10,
|
||||
subtopics=(
|
||||
SubTopic(
|
||||
"locations_endpoints", "Locations & endpoints",
|
||||
(
|
||||
"Location types — WFH / offices.",
|
||||
"Hard phone, soft phone, CODEC, wireless headset?",
|
||||
"Agent greeting / pre-recorded messages, whisper "
|
||||
"announcement.",
|
||||
"PC — desktop, laptop, VDI.",
|
||||
),
|
||||
),
|
||||
SubTopic(
|
||||
"desktop_apps", "Desktop & applications",
|
||||
(
|
||||
"Agent & supervisor desktop — omnichannel? Custom gadgets, "
|
||||
"screen pops, workflows.",
|
||||
"Standard browser? SSO?",
|
||||
"Applications used to handle calls.",
|
||||
"Knowledge Management.",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Topic(
|
||||
key="routing_automation",
|
||||
title="Routing & Automation",
|
||||
scope="Self-service, IVR, speech, virtual agents, agent assist, RPA",
|
||||
minutes=15,
|
||||
subtopics=(
|
||||
SubTopic(
|
||||
"self_service", "Self-service & IVR",
|
||||
(
|
||||
"IVR persona — branding, style guides, voice actors?",
|
||||
"DTMF and/or speech? Speech recognition, TTS, NLU, voice "
|
||||
"biometrics.",
|
||||
"Self-service applications — ID & validate, deflection, "
|
||||
"situational offer (outage / time of day / scheduled).",
|
||||
"API integration (CRM). Intent capture, intent prediction, "
|
||||
"offer push on prediction / account attribute.",
|
||||
),
|
||||
),
|
||||
SubTopic(
|
||||
"virtual_agents", "Virtual agents & assist",
|
||||
(
|
||||
"Virtual agents — which processes?",
|
||||
"Agent assist?",
|
||||
"RPA?",
|
||||
"Current challenges or desired capabilities. Desire to "
|
||||
"automate. DevOps team?",
|
||||
),
|
||||
),
|
||||
SubTopic(
|
||||
"digital_assistants", "Digital assistant apps",
|
||||
(
|
||||
"Alexa, Google Assistant, Siri?",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Topic(
|
||||
key="workforce_engagement",
|
||||
title="Workforce Engagement",
|
||||
scope="Call recording / QM, WFM, agent self-service, integrations",
|
||||
minutes=20,
|
||||
subtopics=(
|
||||
SubTopic(
|
||||
"recording_qm", "Call recording & Quality Management",
|
||||
(
|
||||
"How is call recording used? Compliance, screen capture, "
|
||||
"voice transcription (real-time?), desktop analytics, "
|
||||
"retention period.",
|
||||
"Quality team — who do they report to, how many people? "
|
||||
"Scorecards, score method & metrics, number of assessments, "
|
||||
"coaching, live monitor.",
|
||||
"Locations with call recording. Number of named agents.",
|
||||
),
|
||||
),
|
||||
SubTopic(
|
||||
"workforce_management", "Workforce management",
|
||||
(
|
||||
"Recruitment process, required qualifications, average "
|
||||
"tenure, attrition rate, internal moves.",
|
||||
"Do you measure agent & supervisor experience? Recognition "
|
||||
"program?",
|
||||
"Adherence measure — KPIs, gamification, shift length, "
|
||||
"breaks, annualized utilization %.",
|
||||
"Forecasting & scheduling — peak volume, historical data, "
|
||||
"algorithms, WFM interval (15/30 min).",
|
||||
"Intraday / real-time adherence — what happens when out of "
|
||||
"compliance?",
|
||||
"Payroll integration (ADP / Workday). Satisfaction with "
|
||||
"current tool(s)? Multiskilled agents, FTE calculations.",
|
||||
),
|
||||
),
|
||||
SubTopic(
|
||||
"agent_self_service", "Agent self-service & integrations",
|
||||
(
|
||||
"Agent self-service — absences, shift bids / swaps, "
|
||||
"performance metrics.",
|
||||
"Number of named agents.",
|
||||
"Integration to 3rd party — outsourcer / overflow, payroll.",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Topic(
|
||||
key="training",
|
||||
title="Training",
|
||||
scope="Onboarding, format, assessment, eLearning, QM integration",
|
||||
minutes=5,
|
||||
subtopics=(
|
||||
SubTopic(
|
||||
"onboarding", "Onboarding & enablement",
|
||||
(
|
||||
"Onboarding process — how long, training format, "
|
||||
"assessment?",
|
||||
"eLearning?",
|
||||
"QM integration?",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Topic(
|
||||
key="reporting_insights",
|
||||
title="Reporting & Insights",
|
||||
scope="Real-time & historical reporting, analytics, BI, CRM",
|
||||
minutes=10,
|
||||
subtopics=(
|
||||
SubTopic(
|
||||
"reporting", "Reporting",
|
||||
(
|
||||
"Real-time and historical — what are the key metrics you "
|
||||
"report on?",
|
||||
"Data source integration, dashboards, wallboards, agent & "
|
||||
"supervisor status.",
|
||||
),
|
||||
),
|
||||
SubTopic(
|
||||
"analytics_insights", "Analytics & insights",
|
||||
(
|
||||
"Integrated view of customer details and contact history? "
|
||||
"CRM?",
|
||||
"Does the contact centre collect and use customer insight? "
|
||||
"Predictive engagement?",
|
||||
"What data analysis do you perform? Analytics / BI team — how "
|
||||
"many people? BI platform?",
|
||||
"Executive-level reporting? Marketing team interlock?",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# ── Lookups ──────────────────────────────────────────────────────────
|
||||
TOPIC_BY_KEY: dict[str, Topic] = {t.key: t for t in TOPICS}
|
||||
TOPIC_KEYS: tuple[str, ...] = tuple(t.key for t in TOPICS)
|
||||
|
||||
|
||||
def topic(key: str) -> Topic:
|
||||
"""Return the topic with ``key`` (raises ``KeyError`` if unknown)."""
|
||||
return TOPIC_BY_KEY[key]
|
||||
Reference in New Issue
Block a user