- Introduce WorkflowAgentStage to manage agent stage configurations and enhance interaction with the workflow engine. - Implement WorkflowEdgeEvaluator for priority-aware edge evaluation, improving routing decisions based on conditions and user turns. - Update WorkflowBrain to handle user turns and routing more effectively, ensuring agents cannot have only one default path. - Enhance CallEndCoordinator to track speech events and manage call termination based on queued speech. - Add new models and output handling for workflow interactions, improving clarity and maintainability. - Update tests to validate the new routing logic and agent behavior under various scenarios.
120 lines
3.5 KiB
Python
120 lines
3.5 KiB
Python
"""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)
|
|
)
|