"""Client-visible Workflow output and fixed speech in one place.""" from __future__ import annotations from typing import Any from pipecat.frames.frames import OutputTransportMessageUrgentFrame, TTSSpeakFrame from pipecat.utils.time import time_now_iso8601 from services.brains.base import BrainRuntime from services.runtime_variables import DynamicVariableStore class WorkflowOutput: """Publish debug events and fixed speech without duplicating persistence.""" def __init__( self, store: DynamicVariableStore, runtime: BrainRuntime, ) -> None: self._store = store self._runtime = runtime self._client_ready = False self._pending_transcripts: list[dict[str, Any]] = [] async def mark_client_ready(self) -> None: self._client_ready = True pending = self._pending_transcripts self._pending_transcripts = [] for message in pending: await self.emit(message) async def speak( self, text: str, *, source: str, node_id: str | None = None, ) -> None: """Record, display and synthesize one Workflow-owned utterance.""" content = text.strip() if not content: return self._store.record("agent", content) transcript = { "type": "transcript", "role": "assistant", "content": content, "timestamp": time_now_iso8601(), "source": source, **({"nodeId": node_id} if node_id else {}), } if self._client_ready: await self.emit(transcript) else: self._pending_transcripts.append(transcript) track_speech = getattr(self._runtime.call_end, "track_speech", None) if callable(track_speech): track_speech() await self._runtime.queue_frame( TTSSpeakFrame(content, append_to_context=False) ) async def emit_node_active(self, node_id: str | None) -> None: if node_id: await self.emit({"type": "node-active", "nodeId": node_id}) async def emit_variables( self, *, reason: str, node_id: str | None, changed: list[str] | None = None, ) -> None: message: dict[str, Any] = { "type": "workflow-variables", "reason": reason, "variables": self.public_variables(), } if node_id: message["nodeId"] = node_id if changed: message["changed"] = [ name for name in changed if not name.startswith(("system__", "secret__")) ] await self.emit(message) async def emit_error( self, message: str, *, node_id: str | None, code: str = "workflow_runtime_error", ) -> None: payload: dict[str, Any] = { "type": "workflow-error", "code": code, "message": message, } if node_id: payload["nodeId"] = node_id await self.emit(payload) def public_variables(self) -> dict[str, str | int | float | bool]: return { name: value for name, value in self._store.values.items() if not name.startswith(("system__", "secret__")) and isinstance(value, (str, int, float, bool)) } async def emit(self, message: dict[str, Any]) -> None: await self._runtime.queue_frame( OutputTransportMessageUrgentFrame(message=message) )