This repository has been archived on 2026-09-12. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
praxis/server/assist/latency_metrics.py
T
Praxis CI ec397f2c65 docs(milestone): complete v0.5-live-assist — v0.1.13 tagged, milestone release, merged to main
v0.5 (Live Assist — on-the-job voice companion) milestone complete.
4 phases: P0 (pre-execution, v0.1.10) → P1 (assist core + guardrail,
v0.1.11) → P2 (integration + tech-debt + NFR, v0.1.12) → P3 (final
review + ship, v0.1.13 = milestone release).

16/16 REQs covered (3 ASSIST + 4 NFR + 9 IDEATE). 4 v0.6 backlog.
469 tests passed, 0 failed. 1 P0 fixed (guardrail processor safety).
8 P1+ flagged for v0.6. 8 v0.4 P1+ tech-debt addressed.
G-049 + G-067 grill MUSTs resolved. ESCALATION-01 (PIPEDA) OPEN for
human legal review before assist surface go-live.

---ci---
project: praxis
phase: 3
milestone: v0.5
status: complete
requirements:
  covered: [REQ-ASSIST-01, REQ-ASSIST-02, REQ-ASSIST-03, REQ-NFR-ASSIST-01, REQ-NFR-ASSIST-02, REQ-NFR-ASSIST-03, REQ-NFR-ASSIST-04, REQ-IDEATE-01, REQ-IDEATE-02, REQ-IDEATE-03, REQ-IDEATE-04, REQ-IDEATE-05, REQ-IDEATE-06, REQ-IDEATE-07, REQ-IDEATE-08, REQ-IDEATE-09]
  partial: []
---/ci---
2026-08-04 22:35:56 +00:00

131 lines
4.9 KiB
Python

"""AssistLatencyMetrics — p95 assist-turn latency measurement (TASK-09-01, D-072, REQ-IDEATE-04).
Collects per-turn LatencyRecord objects (from the LatencyObserver — server/latency.py)
and computes the 95th percentile of `e2e_asr_to_tts_ms` (ASR transcript-ready → TTS
first-audio — the C-8 latency budget).
D-072 binding (pilot tolerance):
- target_ms = 600 (C-8 < 600ms — the hard target; v0.6 hardening)
- pilot_tolerance_ms = 650 (≤ 650ms acceptable for pilot per D-072)
- within_target = (p95 < 600) — the v0.6 hardening goal
- within_pilot = (p95 <= 650) — the pilot acceptance gate
The metrics are collected per shift (one AssistLatencyMetrics instance per
AssistSession) and reported at shift-end in the `session_outcome` dict, which
flows to the cohort aggregation (SLICE-10 — `assist_p95_latency_ms` metric).
This module does NOT assert that the actual latency is under budget — that is a
Phase-1 live measurement, not a CI test. This module provides the measurement
infrastructure (collect → percentile → summary). The test (TASK-09-03) asserts
the infrastructure works against mock records.
"""
from __future__ import annotations
import statistics
from typing import Any
from server.latency import LatencyRecord
# D-072 binding thresholds (pilot tolerance).
TARGET_MS = 600 # C-8 hard target (< 600ms — v0.6 hardening goal)
PILOT_TOLERANCE_MS = 650 # D-072 pilot acceptance (≤ 650ms)
def _percentile(values: list[float], pct: float) -> float | None:
"""Compute the `pct`-th percentile (0..100) of `values` using nearest-rank.
Returns None if `values` is empty. Uses the nearest-rank method (the same
method used by numpy's default 'linear' interpolation for integer ranks):
rank = ceil(pct/100 * N), 1-indexed; index = rank - 1 (clamped to [0, N-1]).
This is the standard p95 computation for latency SLOs (Google SRE book §6).
"""
if not values:
return None
s = sorted(values)
n = len(s)
if n == 1:
return s[0]
# Nearest-rank: rank = ceil(pct/100 * n), then index = rank - 1.
import math
rank = max(1, math.ceil((pct / 100.0) * n))
idx = min(rank - 1, n - 1)
return s[idx]
class AssistLatencyMetrics:
"""Collects per-turn latency records + computes p95/p50/p99 (TASK-09-01).
One instance per assist shift. The LatencyObserver (server/latency.py) holds
the live per-turn records; at shift-end the session code calls `record()` for
each completed turn, then `summary()` to get the aggregate dict.
D-072: the summary reports both `within_target` (p95 < 600ms — the v0.6 goal)
and `within_pilot` (p95 ≤ 650ms — the pilot acceptance gate). If
`within_pilot` is False, the shift is flagged for the operator via the cohort
aggregation (`assist_p95_latency_ms` metric — SLICE-10).
"""
def __init__(self) -> None:
self._records: list[LatencyRecord] = []
def record(self, record: LatencyRecord) -> None:
"""Append a latency record (one per completed assist turn)."""
self._records.append(record)
@property
def count(self) -> int:
return len(self._records)
def _e2e_values(self) -> list[float]:
"""The non-None e2e_asr_to_tts_ms values across all records."""
out: list[float] = []
for r in self._records:
v = r.e2e_asr_to_tts_ms
if v is not None:
out.append(float(v))
return out
def p50(self) -> float | None:
"""The median e2e latency (ms), or None if no records."""
return _percentile(self._e2e_values(), 50.0)
def p95(self) -> float | None:
"""The 95th percentile e2e latency (ms), or None if no records."""
return _percentile(self._e2e_values(), 95.0)
def p99(self) -> float | None:
"""The 99th percentile e2e latency (ms), or None if no records."""
return _percentile(self._e2e_values(), 99.0)
def summary(self) -> dict[str, Any]:
"""Return the shift-end latency summary dict (D-072).
Fields:
p50, p95, p99: the percentiles (ms) or None if no records.
count: number of recorded turns.
target_ms: 600 (C-8 hard target).
pilot_tolerance_ms: 650 (D-072 pilot acceptance).
within_target: p95 < 600 (the v0.6 hardening goal).
within_pilot: p95 <= 650 (the pilot acceptance gate).
"""
p50 = self.p50()
p95 = self.p95()
p99 = self.p99()
return {
"p50": p50,
"p95": p95,
"p99": p99,
"count": self.count,
"target_ms": TARGET_MS,
"pilot_tolerance_ms": PILOT_TOLERANCE_MS,
"within_target": (p95 is not None and p95 < TARGET_MS),
"within_pilot": (p95 is not None and p95 <= PILOT_TOLERANCE_MS),
}
__all__ = [
"AssistLatencyMetrics",
"TARGET_MS",
"PILOT_TOLERANCE_MS",
]