Files
Robert Helewka 64fb83257d feat: add GenesysCX study and fix Streamlit chart key collisions
- Add 202512_GenesysCX TEI study (config, seed data, notebooks, README)
  with NPV $10.8M / ROI 266% including AI-token cost line
- Add explicit `key` parameter to all chart wrappers in app/components
  to prevent StreamlitDuplicateElementId errors when the same figure
  type renders across Summary/Benefits/Costs tabs
- Render benefits bar and cost pie charts on their respective tabs
- Add benefits_vs_costs_by_year chart wrapper
2026-06-10 14:26:49 -04:00

88 lines
2.9 KiB
Python

"""
Genesys AI Experience token meters and pricing.
Every meter carries a :class:`Confidence` flag so the UI can distinguish
published Genesys rates from estimates and unknowns. Rates here are
*planning inputs* — this tool explicitly does not replace contractual
pricing (see README, Non-Goals).
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
class MeterType(Enum):
PER_USER_PER_MONTH = "per_user_per_month"
PER_INTERACTION = "per_interaction"
PER_MINUTE = "per_minute"
PER_MESSAGE = "per_message"
PER_SUMMARY = "per_summary"
class Confidence(Enum):
CONFIRMED = "confirmed" # published Genesys rate
ESTIMATED = "estimated" # reasonable industry assumption
UNKNOWN = "unknown" # rate not yet sourced
@property
def icon(self) -> str:
return {"confirmed": "🟢", "estimated": "🟡", "unknown": "🔴"}[self.value]
@dataclass
class TokenMeter:
"""One Genesys AI feature's token meter.
``units_per_token`` and ``tokens_per_unit`` are inverses; both are
stored because the UI shows whichever reads more naturally (e.g.
"17 minutes per token" vs "0.0588 tokens per minute"). For
PER_USER_PER_MONTH meters ``units_per_token`` is 0.0 (n/a) and
``tokens_per_unit`` is the flat tokens/user/month figure.
"""
feature: str
meter_type: MeterType
units_per_token: float
tokens_per_unit: float
confidence: Confidence
notes: str
source_url: str | None = None
def __post_init__(self) -> None:
if self.tokens_per_unit < 0:
raise ValueError(f"{self.feature}: tokens_per_unit must be >= 0")
if (
self.meter_type is not MeterType.PER_USER_PER_MONTH
and self.units_per_token > 0
and self.tokens_per_unit > 0
):
product = self.units_per_token * self.tokens_per_unit
if not 0.95 <= product <= 1.05:
raise ValueError(
f"{self.feature}: units_per_token ({self.units_per_token}) and "
f"tokens_per_unit ({self.tokens_per_unit}) are not inverses"
)
@dataclass
class TokenPricing:
"""Per-region token pricing. Default is US list at $1/token."""
region: str # "US", "AU", "EU", "APAC"
list_rate_per_token: float = 1.0
contracted_rate_per_token: float | None = None
prepay_commit_tokens: int | None = None
overage_rate_per_token: float | None = None
def __post_init__(self) -> None:
if self.list_rate_per_token < 0:
raise ValueError(f"{self.region}: list rate must be >= 0")
def effective_rate(self, use_contracted: bool = False) -> float:
"""Contracted rate when requested and known, else list rate."""
if use_contracted and self.contracted_rate_per_token is not None:
return self.contracted_rate_per_token
return self.list_rate_per_token