Files
praxis/server/services/registry.py
T
Praxis CI 47e24dbe59 feat(P01-02-01): service interfaces — TTSProvider, LLMProvider, Guardrail
server/services/base.py defines three abstract base classes with type
annotations: TTSProvider (async synthesize/synthesize_all, voice_id), LLMProvider
(async chat/chat_full, roleplay_model, debrief_model), Guardrail (async check,
session_start_disclaimer). GuardrailVerdict/Context/TTSResult/LLMStreamChunk
dataclasses carry typed metadata. server/services/registry.py resolves the
active adapter from env (PRAXIS_TTS, PRAXIS_GUARDRAIL) so the pipeline never
imports a concrete adapter directly — D-014/D-019/D-020 swap wiring. Verified:
'from server.services import TTSProvider, LLMProvider, Guardrail' succeeds.

---ci---
phase: 1
milestone: v0.1
plan: 02
task: 02-01
status: execute
persona: backend-engineer
requirements:
  covered: [REQ-VOICE-02, REQ-LLM-01, REQ-ORCH-02]
---/ci---
2026-08-01 13:00:00 +00:00

74 lines
2.5 KiB
Python

"""Adapter registry — resolves the active TTS / LLM / Guardrail from env.
Centralizes the D-014 (TTS swap), D-020 (LLM swap), D-019 (guardrail plug) wiring
so the Pipecat pipeline never imports a concrete adapter directly.
"""
from __future__ import annotations
import os
from functools import lru_cache
def _require(key: str, *, default: str | None = None) -> str:
val = os.environ.get(key, default or "").strip()
if not val:
raise RuntimeError(
f"Required env var {key} is not set. See .env.example."
)
return val
@lru_cache(maxsize=1)
def get_tts() -> "TTSProvider": # type: ignore[name-defined]
"""Return the active TTSProvider based on PRAXIS_TTS (D-014)."""
# Imported lazily so importing the registry doesn't drag in Pipecat/TTS deps
# for tools that only need the interfaces.
choice = os.environ.get("PRAXIS_TTS", "cartesia").strip().lower()
if choice == "piper":
from server.tts.piper_tts import PiperTTS
return PiperTTS()
if choice == "cartesia":
from server.tts.cartesia_tts import CartesiaTTS
return CartesiaTTS()
raise RuntimeError(
f"Unknown PRAXIS_TTS={choice!r}; expected 'cartesia' or 'piper'."
)
@lru_cache(maxsize=1)
def get_llm() -> "LLMProvider": # type: ignore[name-defined]
"""Return the active LLMProvider (Ollama Cloud direct API, D-020)."""
from server.llm.ollama_cloud import OllamaCloudLLM
return OllamaCloudLLM()
@lru_cache(maxsize=1)
def get_guardrail() -> "Guardrail": # type: ignore[name-defined]
"""Return the active Guardrail (D-019).
v0.1 SLICE-02 returns NoOpGuardrail; SLICE-03 swaps in CustomerServiceGuardrail.
Selection via PRAXIS_GUARDRAIL=none|customer_service (default: customer_service
once implemented; falls back to none if the ruleset isn't importable yet).
"""
choice = os.environ.get("PRAXIS_GUARDRAIL", "customer_service").strip().lower()
if choice == "none":
from server.guardrails.noop import NoOpGuardrail
return NoOpGuardrail()
if choice == "customer_service":
try:
from server.guardrails.customer_service import CustomerServiceGuardrail
return CustomerServiceGuardrail()
except ImportError:
# SLICE-02 fallback — real ruleset arrives in SLICE-03.
from server.guardrails.noop import NoOpGuardrail
return NoOpGuardrail()
raise RuntimeError(
f"Unknown PRAXIS_GUARDRAIL={choice!r}; expected 'none' or 'customer_service'."
)