Compliance Metrics¶
Compliance measures whether restaurant staff responded to Emilia's real-time AI suggestions (drink opportunities, dessert opportunities).
The Problem Clustering Solves¶
Emilia lacks context about what's happening at the table. When no one responds to a suggestion, she re-sends the same alert every ~5 minutes. Without clustering:
5 events for the same situation + waiter goes once = ⅕ = 20% compliance
But the waiter DID attend the need → should be ~100%
The hybrid clustering algorithm groups rapid-fire alerts into opportunity moments, so one waiter visit covers the whole burst. But if events happen at genuinely different moments (separated by a waiter visit), they correctly form separate clusters.
Algorithm¶
flowchart TD
A["EchoBase Events<br/>from metadata.tables[].echobase_events"] --> B["Filter by alertType<br/>second-drink-opportunity<br/>dessert-opportunity"]
B --> C["Sort by time per type"]
C --> D{"Gap > 600s?"}
D -->|Yes| E["New Cluster"]
D -->|No| F{"Annotation between<br/>consecutive events?"}
F -->|Yes| E
F -->|No| G["Same Cluster"]
E --> H["Evaluate Cluster"]
G --> H
H --> I{"First annotation<br/>in cluster window?"}
I -->|waiter-touch| J["COMPLIANT"]
I -->|no-waiter-touch| K["NOT COMPLIANT"]
I -->|none found| L["EXCLUDED<br/>events not counted"] Cluster Splitting Rules¶
A cluster boundary is created when either condition is met:
- Time gap > 600 seconds between consecutive events of the same type
- Annotation between events — a
waiter-touchorno-waiter-touchannotation falls between two consecutive events, meaning the situation was acknowledged
Cluster Evaluation: First Annotation Wins¶
For each cluster, the algorithm searches its time window [window_start, window_end] for annotations:
waiter-touchfound first → cluster is COMPLIANTno-waiter-touchfound first → cluster is NOT COMPLIANT- Neither found → cluster is EXCLUDED (all events omitted from counts)
The result is propagated to every event in the cluster.
Real-World Example¶
sequenceDiagram
participant E as Emilia AI
participant W as Waiter
participant A as Annotator
Note over E: Cluster 1 starts
E->>E: second-drink-opportunity at 11980s
E->>E: second-drink-opportunity at 12526s
W->>A: waiter-touch at 12913s
Note over E: Cluster 1 = COMPLIANT
Note over E: Cluster 2 starts
E->>E: second-drink-opportunity at 13795s
E->>E: second-drink-opportunity at 14116s
W->>A: waiter-touch at 14128s
Note over E: Cluster 2 = COMPLIANT Both clusters are compliant. Result: drink_suggestion_count = 4, drink_suggestion_compliance_pct = 100.0.
Output Fields¶
| Field | Type | Description |
|---|---|---|
drink_suggestion_count | int | Count of events (not clusters) for second-drink-opportunity |
drink_suggestion_compliance_pct | float | null | Percentage of compliant drink events. null if count is 0 |
dessert_suggestion_count | int | Count of events for dessert-opportunity |
dessert_suggestion_compliance_pct | float | null | Percentage of compliant dessert events |
Percentages are rounded to 1 decimal place.
real_time_suggestions Detail¶
When compliance events are found, a real_time_suggestions array is added to the session (alongside metrics):
{
"alert_type": "second-drink-opportunity",
"time_seconds": 11980.0,
"alert_timestamp_utc": "2026-03-08T18:05:04Z",
"operator_id": "valeria",
"compliant": true,
"cluster_id": 0
}
This array is included in the fingerprint calculation — changes to suggestion matching trigger a new version even if metrics values are identical.
Changed in version 2.1.0
When a table has no echobase_events, any residual real_time_suggestions from previous enrichment versions are now automatically removed to maintain data consistency between compliance metrics and suggestion detail.
Dessert Compliance Conversion¶
Added in version 2.2.0
Conversion measures whether a compliant dessert suggestion actually resulted in a dessert sale in the POS system. It extends the compliance chain one step further:
Gate Conditions¶
All 4 conditions must be met for conversion to be calculated:
- At least one
dessert-opportunityevent inreal_time_suggestionswithcompliant=True pos_correlation.status == "confirmed"- POS items with valid UTC timestamps available
video_start_utcavailable in shift metadata
When any condition is not met, dessert_compliance_conversion is null.
Causal Alert Selection¶
When multiple compliant dessert clusters exist, the algorithm selects the one with the tightest causal chain:
flowchart TD
A["Compliant dessert-opportunity clusters"] --> B["For each cluster find<br/>waiter-touch that made it compliant"]
B --> C["Find first POS dessert<br/>item offset from video start"]
C --> D{"Any WT before<br/>dessert order?"}
D -->|Yes| E["Pick cluster with WT<br/>closest to dessert order"]
D -->|No| F["Fallback: absolute<br/>closest WT"]
E --> G["delta = dessert_offset - alert_time"]
F --> G
G --> H{"0 <= delta <= window?"}
H -->|Yes| I["conversion = true"]
H -->|No| J["conversion = false"] This ensures the selected alert has the most defensible link to the dessert sale.
Delta Interpretation¶
The raw delta in seconds (dessert_compliance_conversion_delta_seconds) is always stored regardless of the conversion boolean. This allows reporting layers to apply any threshold:
| Delta Value | Meaning |
|---|---|
| Positive, within window | Dessert ordered after alert → conversion |
| Positive, outside window | Dessert ordered too long after alert → no conversion |
| Negative | Dessert ordered before alert → alert was irrelevant |
null | No POS desserts found, or gate not met |
The default conversion window is 1800 seconds (30 minutes), configurable via SESSION_METRICS_DESSERT_CONVERSION_WINDOW_SECONDS.
Items and Revenue¶
Items and revenue count only POS dessert items ordered after the causal cluster's waiter-touch time, not the alert time. This ensures we count desserts that could have been influenced by the waiter's visit.
Output Fields¶
| Field | Type | Default | Description |
|---|---|---|---|
dessert_compliance_conversion | bool | null | null | true if delta is within window. null if gate not met. |
dessert_compliance_conversion_items | int | 0 | Count of POS dessert items ordered after causal waiter-touch |
dessert_compliance_conversion_revenue | float | null | null | Sum of dessert item prices. null if no items converted. |
dessert_compliance_conversion_delta_seconds | int | null | null | Raw seconds from causal alert to first POS dessert order |
Drink Compliance Conversion¶
Added in version 2.3.0
Drink conversion measures whether compliant second-drink-opportunity alerts resulted in additional drink sales in POS. Unlike dessert conversion (a single event), drink conversion evaluates each compliant cluster independently — each is a separate "did the customer order another round?" moment.
Why Per-Cluster?¶
Drink opportunities repeat throughout a meal. A typical session has 3-5 drink alert clusters as the AI detects empty glasses at different moments. Each cluster is an independent upsell opportunity:
sequenceDiagram
participant AI as Emilia AI
participant W as Waiter
participant POS as POS System
Note over AI: Cluster 1 - empty glasses
AI->>W: second-drink-opportunity
W->>W: waiter-touch
POS->>POS: BEER $21 ordered
Note over AI: Cluster 1 = CONVERTED
Note over AI: Cluster 2 - empty glasses again
AI->>W: second-drink-opportunity
W->>W: waiter-touch
Note over POS: No new order
Note over AI: Cluster 2 = NOT CONVERTED
Note over AI: Cluster 3 - empty glasses
AI->>W: second-drink-opportunity
W->>W: waiter-touch
POS->>POS: BEER $21 ordered
Note over AI: Cluster 3 = CONVERTED Result: conversion_count = 2, compliant_clusters = 3, conversion rate = 67%.
Gate Conditions¶
Same as dessert conversion:
- At least one
second-drink-opportunityinreal_time_suggestionswithcompliant=True pos_correlation.status == "confirmed"- POS drink items with valid UTC timestamps available
video_start_utcavailable
When gate not met → drink_compliance_conversion = null.
Cluster Boundary¶
Each cluster's conversion window naturally ends at the next cluster's first alert time (or session end for the last cluster). This prevents attributing a drink order to the wrong alert cycle.
Delta Array for Re-Windowing¶
The drink_compliance_conversion_deltas array stores raw delta seconds for each converted cluster:
This enables flexible time window analysis in reporting without re-enrichment:
-- Conversions within 5 minutes
arrayCount(x -> x >= 0 AND x <= 300, drink_compliance_conversion_deltas)
-- Conversions within 10 minutes
arrayCount(x -> x >= 0 AND x <= 600, drink_compliance_conversion_deltas)
-- Fastest conversion delta
arrayMin(drink_compliance_conversion_deltas)
The default conversion window for the boolean drink_compliance_conversion_count is 900 seconds (15 minutes), configurable via SESSION_METRICS_DRINK_CONVERSION_WINDOW_SECONDS.
Output Fields¶
| Field | Type | Default | Description |
|---|---|---|---|
drink_compliance_conversion | bool | null | null | true if any cluster converted within window. null if gate not met. |
drink_compliance_conversion_count | int | 0 | Clusters that converted within the configured window |
drink_compliance_compliant_clusters | int | 0 | Total compliant clusters evaluated |
drink_compliance_conversion_items | list[int] | [] | POS drink items per converted cluster, parallel to _deltas |
drink_compliance_conversion_revenue | list[float | null] | [] | Revenue per converted cluster, parallel to _deltas. null entry if cluster has no priced items. |
drink_compliance_conversion_deltas | list[int] | [] | Sorted raw delta seconds per converted cluster |