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 bdcf793db2 feat(P02): complete integration + tech-debt + NFR measurement phase — v0.1.12 tagged
Phase 2 (Integration + Tech-Debt + NFR Measurement) complete.
4 slices, 2 waves, 9 tasks. 4 REQs covered. 60 new tests (469 total).
8 v0.4 P1+ tech-debt findings addressed. Verify: APPROVE_WITH_NOTES.

NFR measurement (p95 latency + guardrail FP/FN), cohort aggregation
assist metrics (5 new metrics, no schema change), assist cost tracking
+ C-3 budget check, tech-debt wave (argon2id offload, cookie-secret
validation, credential enum, f-string SQL, cache persistence, zoneinfo,
audit log, 429 mock).

---ci---
project: praxis
phase: 2
milestone: v0.5
status: complete
requirements:
  covered: [REQ-NFR-ASSIST-01, REQ-IDEATE-04, REQ-IDEATE-06, REQ-IDEATE-07]
  partial: []
---/ci---
2026-08-04 22:11:51 +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",
]