db/migrations/0002_debrief.sql documents the explicit SLICE-05 addition of
the debrief_text column to sessions (the column was already in 0001_init.sql
for forward-compatibility; this migration is a marker for history + any
pre-SLICE-05 database). tests/test_debrief_persistence.py verifies both
migrations apply cleanly, the debrief_text column exists, and a completed
session's debrief is retrievable via SELECT debrief_text FROM sessions WHERE
id=?. tests/conftest.py adds the shared tmp_db fixture. Full suite: 70 passed.
---ci---
phase: 1
milestone: v0.1
plan: 05
task: 05-05
status: execute
persona: data-engineer
requirements:
covered: [REQ-DEBRIEF-01, REQ-STATE-01]
---/ci---
server/debrief.py — generate_debrief() loads the session turns + branch
outcome + scenario debrief.debrief_focus, calls deepseek-v4-flash:cloud in
no_think mode (D-020, REQ-LLM-02) with the debrief prompt template
(docs/debrief/default.yaml), produces a concise 3-bullet text summary
(what you did well / what to improve / one next step) referencing the
learner's actual turns + branch outcome.
TASK-05-02: the debrief text is routed through the CustomerServiceGuardrail
output filter (debrief role) — legal-action recommendations are blocked and
replaced with a coaching redirect.
TASK-05-03: the debrief is synthesized via the same TTSProvider interface
as the role-play (D-006 one voice) — no separate TTS path (verified by
structural test).
5 tests pass (debrief references turns + branch, no_think mode asserted,
guardrail blocks 'tell the customer to sue', normal coaching passes,
TTSProvider synthesis reuses role-play voice).
---ci---
phase: 1
milestone: v0.1
plan: 05
task: 05-01,05-02,05-03
status: execute
persona: backend-engineer
requirements:
covered: [REQ-DEBRIEF-01, REQ-LLM-02, REQ-NFR-SAFE-01]
---/ci---
server/interruptibility.py — programmatic check that the pipeline task is
configured with allow_interruptions=True (D-008 abort-and-yield). The manual
test (speaking during AI speech cuts it off) is documented in
docs/latency-report.md; the automated test confirms the flag is set.
server/scenarios/classifier.py — classify_branch() uses
deepseek-v4-flash:cloud no-think (D-020) as LLM-as-judge to classify the
learner's turn transcripts into accept_resolution or escalate at session end,
OFFLINE from the voice loop (D-P1-05, G-002 post-hoc classification). Parses
the LLM's JSON response leniently (code fences, unknown-id fallback, malformed
JSON scan). classify_branch_sync_heuristic() is a rule-based fallback for
tests/e2e when no API key is present. 11 tests pass (interruptibility flag
on/off/missing; classifier accept/escalate scripted transcripts; JSON parse
valid/code-fenced/unknown-id/malformed; fake-LLM async classify; offline
structural assertion).
---ci---
phase: 1
milestone: v0.1
plan: 03
task: 03-05,03-06
status: execute
persona: backend-engineer
requirements:
covered: [REQ-VOICE-04, REQ-SCEN-01]
---/ci---
server/guardrails/customer_service.py implements the Guardrail interface with
the v0.1 Customer Service ruleset (RESEARCH.md safety baseline): blocks
legal/financial/medical advice, real-company impersonation, and a debrief
output filter that blocks recommendations the learner advise legal action
('tell the customer to sue them' → blocked_legal with a coaching redirect).
Session-start disclaimer text defined ('This is an AI practice session…').
Selected via PRAXIS_GUARDRAIL=customer_service (default); swaps in for
NoOpGuardrail with no pipeline change (D-019). server/__main__.py loads the
guardrail and logs the disclaimer as the first AI utterance. 9 unit tests
pass (disclaimer, blocks legal/financial/medical, allows normal coaching,
debrief filter blocks 'sue them', debrief allows coaching, swappable with
NoOp).
---ci---
phase: 1
milestone: v0.1
plan: 03
task: 03-04
status: execute
persona: backend-engineer
requirements:
covered: [REQ-ORCH-02, REQ-NFR-SAFE-01]
---/ci---
server/scenarios/runtime.py — ScenarioRuntime maps a Scenario to a Pipecat
Flows state-machine spec: initial 'conversation' state with the scenario's
system prompt + opening line, branch metadata carried in the spec (v0.1
has no in-flight transitions per G-002 — branching is post-hoc; Phase 2+
can fork without schema change). set_branch() resolves the branch outcome
from the classifier; debrief_focus() returns the per-branch focus.
TASK-03-07: server/pipeline.py build_pipeline() now accepts a scenario_id,
loads the runtime, and uses scenario.setup.system_prompt instead of the
SLICE-02 hardcoded walking-skeleton prompt. Falls back gracefully if the
scenario can't load. server/__main__.py passes
PRAXIS_SCENARIO=customer_service_refund_ca_v01 by default. 7 runtime tests
pass (system prompt, set_branch accept/escalate, unknown-branch error,
debrief_focus per branch, flows spec branches, debrief model config).
---ci---
phase: 1
milestone: v0.1
plan: 03
task: 03-03,03-07
status: execute
persona: backend-engineer
requirements:
covered: [REQ-SCEN-01, REQ-SCEN-FMT-01, REQ-ORCH-02]
---/ci---
client/ — React + Vite + TypeScript scaffolded with the Pipecat client SDK
(@pipecat-ai/client-js) and SmallWebRTCTransport
(@pipecat-ai/small-webrtc-transport). useVoiceSession.ts hook manages mic
permission, WebRTC connect, audio playback, live transcript, and a latency
readout (captures the e2e_latency_ms metric the server emits). App.tsx is a
minimal one-page session UI: disclaimer, Start/End buttons, status badge,
latency readout (within/over 600ms budget), live transcript. vite.config.ts
proxies /pipecat + /health to the Python server (port 8789). npm run
typecheck + npm run build pass.
server/latency.py — LatencyObserver (a Pipecat FrameProcessor) timestamps
transcript-ready, LLM-first-token, TTS-first-audio, and playback-start per
turn, computes ASR→TTS-first-audio (the v0.1 latency target), and logs it
to console with a within/over-budget verdict. Wired into the pipeline
between STT/LLM/TTS so it observes without altering the frame stream. 5
unit tests pass (LatencyRecord e2e math + observer construction +
reset_turn). Full server suite: 18 passed.
---ci---
phase: 1
milestone: v0.1
plan: 02
task: 02-05,02-06
status: execute
persona: frontend-engineer,backend-engineer
requirements:
covered: [REQ-VOICE-01, REQ-VOICE-02, REQ-VOICE-03, REQ-NFR-LAT-01]
---/ci---
server/guardrails/noop.py implements the Guardrail interface with an
always-allow stub so the Pipecat pipeline has the pluggable hook from
SLICE-02. Swapping to CustomerServiceGuardrail (SLICE-03 TASK-03-04)
requires no pipeline change (D-019). The session-start disclaimer text
(RESEARCH.md safety baseline) is defined here so the pipeline can play
it as the first AI utterance even before the real ruleset lands.
---ci---
phase: 1
milestone: v0.1
plan: 02
task: 02-07
status: execute
persona: backend-engineer
requirements:
covered: [REQ-ORCH-02]
---/ci---
server/llm/ollama_cloud.py wraps the Ollama Cloud direct API
(https://ollama.com/api/chat + bearer, stream=True) behind LLMProvider.
chat() streams LLMStreamChunk (is_first flag for TTFT measurement);
chat_full() accumulates for the debrief / branch classifier (offline).
Two models: gemma4:cloud (roleplay_model) + deepseek-v4-flash:cloud
(debrief_model, no_think mode for latency, D-020). Resolves R6 — the
adapter confirms the direct API + bearer path; a live first-token
confirmation is pending the R3 probe with a real key. Graceful no-key
degradation (no chunks, no crash). 6 unit tests pass (mocked httpx
streaming response + env model selection + chat_full accumulation).
---ci---
phase: 1
milestone: v0.1
plan: 02
task: 02-03
status: execute
persona: backend-engineer
requirements:
covered: [REQ-LLM-01, REQ-LLM-02]
---/ci---
server/tts/cartesia_tts.py wraps the raw Cartesia Sonic WebSocket API
(wss://api.cartesia.ai/tts/websocket) — streaming PCM s16le, one voice
(CARTESIA_VOICE_ID, D-006). server/tts/piper_tts.py wraps piper-tts
self-hosted synthesis (R4 mitigation, open-weights). Both implement
TTSProvider (synthesize streaming + synthesize_all). Both degrade
gracefully (no audio, no crash) when the API key / voice model is absent.
PRAXIS_TTS=cartesia|piper selects the adapter via the registry with no
pipeline change (D-014). 7 unit tests pass (mock streams + env selection
+ graceful missing-key/model handling).
---ci---
phase: 1
milestone: v0.1
plan: 02
task: 02-02
status: execute
persona: backend-engineer
requirements:
covered: [REQ-VOICE-02]
---/ci---
docs/latency-report.md documents the SLICE-01 spike status: probe
infrastructure is built and ready; live measurements are pending API key
provisioning (only GITEA_TOKEN exists at EXECUTE time). Includes the latency
budget, the TTS decision matrix (Cartesia vs Piper, pending live R4 numbers),
the SLICE-01 go/no-go gate no-go actions per G-003, Piper pre-staging
instructions, and the R6 resolution (Pipecat OLLamaLLMService accepts custom
base_url + bearer). TTS decision recorded as 'pending live measurement —
Piper pre-staged as R4 mitigation per ARCHITECTURE.md'.
---ci---
phase: 1
milestone: v0.1
plan: 01
task: 01-06
status: execute
persona: lead-developer
requirements:
covered: [REQ-VOICE-03, REQ-NFR-LAT-01, REQ-LLM-01, REQ-LLM-02]
---/ci---
scripts/probe_e2e.py runs the integrated path: ASR-transcript-in → Ollama
gemma4:cloud streaming → Cartesia TTS streaming; measures end-to-end
(transcript-in → first-audio-out) over N iterations (default 10). Prints
per-segment (LLM TTFT, TTS first-audio) + e2e summaries, and a budget
comparison vs the 600ms target. The Piper leg is optional (--piper) and
documents the pre-staged R4 mitigation if the voice model isn't provisioned.
If keys are missing, prints KEY_MISSING and exits 0.
---ci---
phase: 1
milestone: v0.1
plan: 01
task: 01-05
status: execute
persona: backend-engineer
requirements:
covered: [REQ-VOICE-03, REQ-NFR-LAT-01, REQ-LLM-01, REQ-LLM-02]
---/ci---
scripts/probe_ollama.py calls https://ollama.com/api/chat with bearer auth,
stream=True, for gemma4:cloud (role-play) and deepseek-v4-flash:cloud
(no_think mode, D-020). Measures time-to-first-token over N iterations
(default 20), prints min/median/p95/mean tables for both models, and logs
any error/throttle events (R5). Also resolves R6 — a successful probe
confirms the direct API + bearer token path works. If OLLAMA_API_KEY is
missing, prints KEY_MISSING and exits 0.
---ci---
phase: 1
milestone: v0.1
plan: 01
task: 01-04
status: execute
persona: backend-engineer
requirements:
covered: [REQ-LLM-01, REQ-LLM-02, REQ-VOICE-03, REQ-NFR-LAT-01]
---/ci---
scripts/probe_cartesia.py opens a WebSocket to Cartesia Sonic, requests TTS
for a sample customer-service utterance, and measures first-audio-byte
latency over N iterations (default 20). Prints min/median/p95/mean. Uses the
raw Cartesia WS API (no SDK coupling). If CARTESIA_API_KEY is missing, prints
KEY_MISSING and exits 0. Also fixes the R1 Deepgram probe to use the raw WS
API instead of the churn-prone SDK listen client.
---ci---
phase: 1
milestone: v0.1
plan: 01
task: 01-03
status: execute
persona: backend-engineer
requirements:
covered: [REQ-VOICE-03, REQ-NFR-LAT-01]
---/ci---
scripts/probe_deepgram.py opens a streaming WebSocket to Deepgram Nova-3,
sends synthesized 16kHz mono PCM in 100ms chunks, and measures
first-partial-transcript latency over N iterations (default 20). Prints
min/median/p95/mean. If DEEPGRAM_API_KEY is missing, prints a clear
KEY_MISSING banner and exits 0 — the probe infrastructure is the deliverable;
live numbers come when keys are provisioned.
---ci---
phase: 1
milestone: v0.1
plan: 01
task: 01-02
status: execute
persona: backend-engineer
requirements:
covered: [REQ-VOICE-03, REQ-NFR-LAT-01]
---/ci---