Mercury Genesys Token Calculator

This commit is contained in:
2026-08-07 14:49:39 -04:00
parent 22a5d907d6
commit 89b835d681
35 changed files with 14105 additions and 2912 deletions

View File

@@ -0,0 +1,270 @@
"""Orchestration — one call in, the whole costed picture out.
The notebook assembles :class:`CalculatorInputs` from its ``engagement-data``
cell and its widgets, calls :func:`calculate`, and renders the result. The
engine never invents a volume and never reads a widget.
:attr:`CalculatorResult.warnings` is deliberate: when a published rule makes
a number smaller — Copilot covering summaries, the allowance absorbing a
month, deflection zeroing TTS — the result says so. A total that shrank for
a reason nobody can see is indistinguishable from a bug.
"""
from __future__ import annotations
from dataclasses import dataclass, field, replace
from typing import Any
from .billing import CostLine, CostTotals, cost_lines, total_cost
from .meters import LicenceModel, MeterBasis, TokenPrice
from .ratecard import meter
from .tts import TtsLine, TtsUsage, tts_cost
from .usage import (
DeflectionMix,
VoiceBotUsage,
Volumes,
apply_copilot_covers_summary,
subscription_tokens,
virtual_agent_tokens,
voice_bot_billable_minutes,
voice_bot_roundup_uplift,
voice_bot_tokens,
)
#: Meter keys whose units come straight off a channel volume. The notebook's
#: feature catalogue decides which are switched on; this maps each to the
#: volume that drives it.
_VOLUME_DRIVEN: dict[str, str] = {
"bots_digital": "digital_sessions_monthly",
"ai_summary_and_insights": "voice_total_monthly",
"ai_scoring": "evaluations_monthly",
"ai_translate": "translations_monthly",
"predictive_routing": "routed_interactions_monthly",
"genesys_cloud_copilot": "ai_actions_monthly",
"apple_messages_for_business": "messaging_monthly",
"facebook_messenger": "messaging_monthly",
"instagram_direct_messaging": "messaging_monthly",
"whatsapp_messaging": "messaging_monthly",
"x_direct_messaging": "messaging_monthly",
"genesys_cloud_social": "social_posts_monthly",
"social_post_responses": "social_responses_monthly",
"predictive_engagement": "all_interactions_monthly",
"knowledge_queries": "all_interactions_monthly",
}
#: The per-user subscription meters, by licence-model face.
_PER_USER_KEYS: frozenset[str] = frozenset(
{
"agent_copilot_named",
"agent_copilot_concurrent",
"speech_and_text_analytics_named",
"speech_and_text_analytics_concurrent",
}
)
#: Deflection-driven meters — priced from the highest-tier partition, not a
#: raw volume.
_TIER_KEYS: frozenset[str] = frozenset({"virtual_agent", "agentic_virtual_agent"})
#: The Copilot family, in either licence face.
_COPILOT_KEYS: frozenset[str] = frozenset(
{"agent_copilot_named", "agent_copilot_concurrent"}
)
def _volume_for(volumes: Volumes, attr: str) -> float:
return float(getattr(volumes, attr))
@dataclass(frozen=True)
class CalculatorInputs:
"""Everything the notebook supplies. Client data plus widget state."""
volumes: Volumes
users: int
licence: LicenceModel = LicenceModel.NAMED
enabled: frozenset[str] = frozenset()
mix: DeflectionMix = field(default_factory=DeflectionMix)
price: TokenPrice = field(default_factory=TokenPrice)
voice_bot: VoiceBotUsage | None = None
tts: TtsUsage | None = None
apply_allowance: bool = True
def __post_init__(self) -> None:
if self.users < 0:
raise ValueError("users must not be negative")
unknown = sorted(k for k in self.enabled if k not in _known_keys())
if unknown:
raise ValueError(
f"enabled contains keys that are not published meters: {unknown}"
)
@property
def copilot_enabled(self) -> bool:
return bool(self.enabled & _COPILOT_KEYS)
def with_(self, **changes: Any) -> CalculatorInputs:
"""A copy with fields replaced — for scenarios and sensitivity."""
return replace(self, **changes)
def _known_keys() -> frozenset[str]:
from .ratecard import METERS
return frozenset(METERS)
@dataclass(frozen=True)
class CalculatorResult:
"""The costed picture, plus why any number shrank."""
lines: tuple[CostLine, ...]
totals: CostTotals
tts: TtsLine | None
grand_total_annual: float
warnings: tuple[str, ...]
@property
def token_cost_annual(self) -> float:
return self.totals.token_cost_annual
@property
def tts_cost_annual(self) -> float:
return self.tts.cost_annual if self.tts is not None else 0.0
def metered_units(inputs: CalculatorInputs) -> dict[str, float]:
"""Monthly metered units per enabled meter key, before the Copilot rule."""
units: dict[str, float] = {}
for key in sorted(inputs.enabled):
if key in _PER_USER_KEYS:
units[key] = float(inputs.users)
elif key in _TIER_KEYS:
share = (
inputs.mix.virtual_agent_share
if key == "virtual_agent"
else inputs.mix.agentic_va_share
)
units[key] = inputs.volumes.voice_total_monthly * share
elif key == "bots_voice":
units[key] = (
voice_bot_billable_minutes(inputs.voice_bot)
if inputs.voice_bot is not None
else 0.0
)
elif key in _VOLUME_DRIVEN:
units[key] = _volume_for(inputs.volumes, _VOLUME_DRIVEN[key])
else: # pragma: no cover — every published key is classified above
raise ValueError(f"{key} has no unit driver")
return units
def calculate(inputs: CalculatorInputs) -> CalculatorResult:
"""Price a scenario end to end."""
raw_units = metered_units(inputs)
usage = apply_copilot_covers_summary(raw_units, inputs.copilot_enabled)
units = usage.units
warnings: list[str] = list(usage.notes)
tokens: dict[str, float] = {}
consumption = 0.0
# Per-user subscription meters — exact, never rounded.
per_user_enabled = frozenset(inputs.enabled & _PER_USER_KEYS)
subs = subscription_tokens(inputs.users, per_user_enabled, inputs.licence)
tokens.update(subs)
subscription = sum(subs.values())
# Voice bots — minutes with the published per-call round-up.
if "bots_voice" in inputs.enabled and inputs.voice_bot is not None:
bot_tokens = voice_bot_tokens(inputs.voice_bot)
tokens["bots_voice"] = bot_tokens
consumption += bot_tokens
uplift = voice_bot_roundup_uplift(inputs.voice_bot)
if uplift > 0:
warnings.append(
f"Voice bots: the published 15-second per-call round-up adds "
f"{uplift:.1%} to billable bot minutes at a "
f"{inputs.voice_bot.avg_bot_seconds_per_call:.0f}s average."
)
# Virtual agents — the highest-tier partition of voice volume.
tier_tokens = virtual_agent_tokens(
inputs.volumes.voice_total_monthly, inputs.mix
)
for key in sorted(_TIER_KEYS & inputs.enabled):
tokens[key] = tier_tokens.get(key, 0.0)
consumption += tokens[key]
# Everything else — units × published rate.
for key in sorted(inputs.enabled):
if key in _PER_USER_KEYS or key in _TIER_KEYS or key == "bots_voice":
continue
m = meter(key)
value = m.tokens_for(units.get(key, 0.0))
tokens[key] = value
consumption += value
if m.basis is MeterBasis.ZERO_RATED and units.get(key, 0.0) > 0:
warnings.append(
f"{m.feature}: {m.published_rate} — shown at $0 because "
"Genesys publishes it as included."
)
totals = total_cost(
consumption, subscription, inputs.licence, inputs.price, inputs.apply_allowance
)
if inputs.apply_allowance and totals.billable_tokens_monthly == 0:
warnings.append(
f"The free monthly allowance ({totals.allowance_tokens_monthly} "
f"tokens, {inputs.licence.label.lower()} org) covers this month's "
"consumption entirely."
)
tts_line = (
tts_cost(inputs.tts, inputs.mix, inputs.price.concession_pct)
if inputs.tts is not None
else None
)
if tts_line is not None:
warnings.extend(tts_line.warnings)
lines = cost_lines(tokens, units, inputs.price)
grand_total = totals.token_cost_annual + (
tts_line.cost_annual if tts_line is not None else 0.0
)
return CalculatorResult(
lines=lines,
totals=totals,
tts=tts_line,
grand_total_annual=grand_total,
warnings=tuple(warnings),
)
def cost_per_interaction(result: CalculatorResult, volumes: Volumes) -> float:
"""Annual grand total ÷ annual interactions — the number clients recall."""
annual = volumes.all_interactions_monthly * 12
if annual == 0:
return 0.0
return result.grand_total_annual / annual
def compare(scenarios: dict[str, CalculatorInputs]) -> list[dict[str, Any]]:
"""One row per scenario. Cost only — a calculator does not build a case."""
rows: list[dict[str, Any]] = []
for name, inputs in scenarios.items():
result = calculate(inputs)
rows.append(
{
"Scenario": name,
"Billable tokens / mo": result.totals.billable_tokens_monthly,
"Token cost / yr": result.totals.token_cost_annual,
"TTS cost / yr": result.tts_cost_annual,
"Total / yr": result.grand_total_annual,
"$ / interaction": cost_per_interaction(result, inputs.volumes),
}
)
return rows