diff --git a/app/ai/voice/agents/breeze_buddy/agent/pipeline.py b/app/ai/voice/agents/breeze_buddy/agent/pipeline.py index 243e5de35..6a83c09c9 100644 --- a/app/ai/voice/agents/breeze_buddy/agent/pipeline.py +++ b/app/ai/voice/agents/breeze_buddy/agent/pipeline.py @@ -33,6 +33,7 @@ MinWordsUserTurnStartStrategy, TranscriptionUserTurnStartStrategy, VADUserTurnStartStrategy, + WakePhraseUserTurnStartStrategy, ) from pipecat.turns.user_stop import ( BaseUserTurnStopStrategy, @@ -354,6 +355,27 @@ async def build_pipeline( # --- User turn start strategies --- start_strategies: list[BaseUserTurnStartStrategy] = [] + + # Wake phrase: prepended first so it gates all subsequent strategies. + wake_cfg = getattr(configurations, "wake_phrase", None) + if wake_cfg and wake_cfg.enabled: + if is_realtime: + logger.warning( + "Wake phrase is not supported in realtime mode. wake_phrase.enabled will be ignored." + ) + elif wake_cfg.phrases: + start_strategies.append( + WakePhraseUserTurnStartStrategy( + phrases=wake_cfg.phrases, + timeout=wake_cfg.timeout, + single_activation=wake_cfg.single_activation, + ) + ) + logger.info( + f"WakePhrase: enabled with {len(wake_cfg.phrases)} phrase(s), " + f"single_activation={wake_cfg.single_activation}, timeout={wake_cfg.timeout}s" + ) + if vad_analyzer is not None: start_strategies.append(VADUserTurnStartStrategy()) diff --git a/app/ai/voice/agents/breeze_buddy/agent/utils.py b/app/ai/voice/agents/breeze_buddy/agent/utils.py index cae7b9d62..9f18eec39 100644 --- a/app/ai/voice/agents/breeze_buddy/agent/utils.py +++ b/app/ai/voice/agents/breeze_buddy/agent/utils.py @@ -268,6 +268,20 @@ def validate_template_compat(template: TemplateModel) -> None: "realtime." ) + wake_phrase = ( + getattr(configurations, "wake_phrase", None) + if configurations is not None + else None + ) + if wake_phrase is not None and getattr(wake_phrase, "enabled", False): + raise ValueError( + "Realtime LLMs (llm_configurations.realtime set) cannot be " + "combined with configurations.wake_phrase — the realtime pipeline " + "builds its own turn strategies and WakePhraseUserTurnStartStrategy " + f"is never installed. Disable wake_phrase on template {template.id} " + "or disable realtime." + ) + logger.info( f"Template {template.id} validated: realtime LLM + direct mode " f"(provider={realtime.provider.value})" diff --git a/app/ai/voice/agents/breeze_buddy/template/types.py b/app/ai/voice/agents/breeze_buddy/template/types.py index ec681580e..0f297bf46 100644 --- a/app/ai/voice/agents/breeze_buddy/template/types.py +++ b/app/ai/voice/agents/breeze_buddy/template/types.py @@ -403,6 +403,36 @@ class KeywordMatchType(str, Enum): INCLUDES = "includes" # Transcription must contain the keyword (case-insensitive) +class WakePhraseConfig(BaseModel): + """Require a wake phrase before the bot responds. + + Wraps pipecat's WakePhraseUserTurnStartStrategy. Placed first in start + strategies so no other strategy evaluates until the phrase is heard. + + Example:: + + {"enabled": true, "phrases": ["yes", "haan"], "single_activation": true} + """ + + enabled: bool = False + phrases: List[str] = Field(default_factory=list, min_length=1) + timeout: float = Field( + 10.0, ge=0.0, le=300.0, description="Seconds to stay awake after phrase." + ) + single_activation: bool = Field( + False, + description="If true, wake phrase required only once per session; if false, required before every turn.", + ) + + @model_validator(mode="after") + def validate_phrases_when_enabled(self): + if self.enabled and not self.phrases: + raise ValueError( + "phrases must contain at least one item when enabled is true" + ) + return self + + class KeywordFilterConfig(BaseModel): """Configuration for filtering out specific transcriptions during bot activity. @@ -886,6 +916,10 @@ def _pre_validate(cls, data: Any) -> Any: None, description="Keyword filter to suppress specific transcriptions while bot is active", ) + wake_phrase: Optional[WakePhraseConfig] = Field( + None, + description="Wake phrase config — bot only responds after hearing a trigger phrase", + ) mcp: Optional[McpConfig] = Field( None, description="MCP tool server configuration for dynamic tool discovery",