81d43666c7
Phase 1 (Assist Core + Guardrail) complete. 8 slices, 4 waves, 24 tasks. 12 REQs covered (3 ASSIST + 3 NFR + 6 IDEATE). 92 new tests (409 total). G-049 + G-067 MUSTs resolved. Verify: APPROVE_WITH_NOTES, 5 P1+ flagged. Live Assist voice loop: shift-bounded sessions, context-binding, 3-layer guardrail (prompt + regex filter + audit log), tap-to-talk client control, warm WebRTC, reconnect logic, incremental audit write, PII policy, consent disclosure, mode-conflict enforcement. ---ci--- project: praxis phase: 1 milestone: v0.5 status: complete requirements: covered: [REQ-ASSIST-01, REQ-ASSIST-02, REQ-ASSIST-03, REQ-NFR-ASSIST-02, REQ-NFR-ASSIST-03, REQ-NFR-ASSIST-04, REQ-IDEATE-01, REQ-IDEATE-02, REQ-IDEATE-03, REQ-IDEATE-05, REQ-IDEATE-08, REQ-IDEATE-09] partial: [] ---/ci---
134 lines
5.1 KiB
Python
134 lines
5.1 KiB
Python
"""build_assist_pipeline — the assist-mode Pipecat pipeline (D-061, D-065, D-066, TASK-05-01).
|
|
|
|
Reuses the v0.1 voice services (_build_transport, _build_stt, _build_llm from
|
|
server/pipeline.py — FIXED, not rewritten). Swaps the system prompt for the
|
|
≤150-token assist prompt (AssistContextBinder). Defaults to Piper TTS for
|
|
assist (D-065 — ~80ms first audio vs Cartesia ~120ms). Inserts the
|
|
LiveAssistGuardrailProcessor between llm and tts (D-060 layer 2).
|
|
|
|
Pipeline structure:
|
|
transport.input() → stt → latency_observer → user_aggregator → llm →
|
|
latency_observer → LiveAssistGuardrailProcessor → tts → latency_observer →
|
|
transport.output() → assistant_aggregator
|
|
|
|
No opening line (assist is invoked mid-shift — no scripted opener, unlike
|
|
practice which plays the scenario opening line).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Any
|
|
|
|
from loguru import logger
|
|
|
|
from server.assist.context import AssistContext
|
|
from server.assist.guardrail_processor import LiveAssistGuardrailProcessor
|
|
from server.guardrails.live_assist import LiveAssistGuardrail
|
|
|
|
|
|
def _env(key: str, default: str = "") -> str:
|
|
return os.environ.get(key, default).strip()
|
|
|
|
|
|
def _build_tts_piper() -> Any:
|
|
"""Build the Piper TTS service (D-065 — default for assist, ~80ms first audio)."""
|
|
from pipecat.services.piper.tts import PiperTTSService
|
|
|
|
voice_model = _env("PIPER_VOICE_MODEL")
|
|
if not voice_model:
|
|
logger.warning("PIPER_VOICE_MODEL not set — Piper TTS will not speak (pipeline still starts).")
|
|
return PiperTTSService(voice_id=voice_model or "missing")
|
|
|
|
|
|
def _build_tts_assist() -> Any:
|
|
"""Build the TTS service for assist mode (D-065).
|
|
|
|
Default: Piper (self-hosted, ~80ms). Fallback: Cartesia if
|
|
PRAXIS_ASSIST_TTS=cartesia (for testing without Piper).
|
|
"""
|
|
choice = _env("PRAXIS_ASSIST_TTS", "piper").lower()
|
|
if choice == "cartesia":
|
|
from server.pipeline import _build_tts
|
|
|
|
return _build_tts() # Cartesia (practice path)
|
|
return _build_tts_piper()
|
|
|
|
|
|
def build_assist_pipeline(
|
|
webrtc_connection,
|
|
*,
|
|
context: AssistContext,
|
|
guardrail: LiveAssistGuardrail | None = None,
|
|
session: Any | None = None,
|
|
) -> tuple:
|
|
"""Assemble the assist-mode Pipecat pipeline (TASK-05-01, D-061, D-065, D-066).
|
|
|
|
Reuses _build_transport, _build_stt, _build_llm from server/pipeline.py.
|
|
Uses Piper TTS by default (D-065). Inserts the LiveAssistGuardrailProcessor
|
|
between llm and tts. No opening line (assist is invoked mid-shift).
|
|
|
|
Returns (pipeline, task, runner, transport) — no scenario_runtime (assist
|
|
has an AssistContext, not a ScenarioRuntime).
|
|
"""
|
|
from pipecat.pipeline.pipeline import Pipeline
|
|
from pipecat.pipeline.runner import PipelineRunner
|
|
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
|
from pipecat.processors.aggregators.llm_context import LLMContext
|
|
from pipecat.processors.aggregators.llm_response_universal import (
|
|
LLMContextAggregator,
|
|
)
|
|
|
|
from server.latency import LatencyObserver
|
|
from server.pipeline import _build_llm, _build_stt, _build_transport
|
|
|
|
transport = _build_transport(webrtc_connection)
|
|
stt = _build_stt()
|
|
llm = _build_llm()
|
|
tts = _build_tts_assist()
|
|
|
|
latency_observer = LatencyObserver()
|
|
|
|
# Build the LLM context from the ≤150-token assist prompt (D-066).
|
|
llm_context = LLMContext(messages=[{"role": "system", "content": context.system_prompt}])
|
|
user_aggregator = LLMContextAggregator(context=llm_context, role="user")
|
|
assistant_aggregator = LLMContextAggregator(context=llm_context, role="assistant")
|
|
|
|
# In-loop guardrail processor (D-060 layer 2, REQ-IDEATE-02).
|
|
if guardrail is None:
|
|
guardrail = LiveAssistGuardrail()
|
|
guardrail_processor = LiveAssistGuardrailProcessor(
|
|
guardrail=guardrail, session=session, llm_context=llm_context
|
|
)
|
|
|
|
pipeline = Pipeline(
|
|
[
|
|
transport.input(), # WebRTC audio in
|
|
stt, # Deepgram Nova-3
|
|
latency_observer, # timestamp ASR-ready
|
|
user_aggregator, # collect user transcript into context
|
|
llm, # Ollama gemma4:cloud (assist prompt)
|
|
latency_observer, # timestamp LLM-first-token
|
|
guardrail_processor, # LiveAssistGuardrail (post-LLM, pre-TTS)
|
|
tts, # Piper (default) or Cartesia
|
|
latency_observer, # timestamp TTS-first-audio
|
|
transport.output(), # WebRTC audio out
|
|
assistant_aggregator, # collect assistant text into context
|
|
]
|
|
)
|
|
|
|
task = PipelineTask(
|
|
pipeline,
|
|
params=PipelineParams(
|
|
allow_interruptions=True, # D-008 abort-and-yield
|
|
enable_metrics=True, # latency measurement
|
|
metrics_request_timeout=10.0,
|
|
),
|
|
)
|
|
|
|
runner = PipelineRunner(handle_sigint=False)
|
|
# No opening line — assist is invoked mid-shift (no scripted opener).
|
|
return pipeline, task, runner, transport
|
|
|
|
|
|
__all__ = ["build_assist_pipeline"] |