Files
ai-video-fullstack/backend/services/workflow/output.py
2026-08-01 11:21:31 +08:00

144 lines
4.2 KiB
Python

"""Client-visible Workflow output and fixed speech in one place."""
from __future__ import annotations
from typing import Any
from uuid import uuid4
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_trace(
self,
event: str,
*,
revision: str,
transition_id: int,
**details: Any,
) -> None:
"""Publish one ordered, machine-readable Workflow runtime event."""
await self.emit(
{
"type": "workflow-event",
"eventId": f"wfe_{uuid4().hex[:20]}",
"event": event,
"timestamp": time_now_iso8601(),
"sessionId": self._runtime.session_id,
"workflowRevision": revision,
"transitionId": transition_id,
**details,
}
)
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)
)