refactor(rtvi): clarify UI message names

This commit is contained in:
Mark Backman
2026-05-06 11:08:25 -04:00
parent 43abca0b06
commit 41124dc494
5 changed files with 39 additions and 22 deletions

View File

@@ -33,28 +33,26 @@ class RTVIUICommandFrame(SystemFrame):
"""A frame for sending a UI command to the client.
Pipeline-side counterpart of the ``ui-command`` RTVI message.
The observer wraps the ``command_name`` + ``payload`` into a
The observer wraps the ``command`` + ``payload`` into a
``UICommandMessage`` envelope before pushing it to the transport,
so the wire shape is:
``{label, type: "ui-command", data: {name, payload}}``.
``{label, type: "ui-command", data: {command, payload}}``.
Parameters:
command_name: App-defined command name (e.g. ``"toast"``,
``"navigate"``, or any app-specific name). The wire
field is ``data.name``; this avoids shadowing
``SystemFrame.name`` (the per-instance debug label).
command: App-defined command (e.g. ``"toast"``,
``"navigate"``, or any app-specific command).
payload: App-defined payload. Pydantic command models
(``Toast``, ``Navigate``, ``ScrollTo``, ...) should be
converted to a plain dict via ``model_dump()`` before
being placed here; an arbitrary dict works as well.
"""
command_name: str = ""
command: str = ""
payload: Any = None
def __str__(self):
"""String representation of the UI command frame."""
return f"{self.name}(command: {self.command_name})"
return f"{self.name}(command: {self.command})"
@dataclass
@@ -97,17 +95,17 @@ class RTVIUIEventFrame(SystemFrame):
Parameters:
msg_id: The RTVI message id, as set by the client.
event_name: App-defined event name (the ``data.name`` field).
event: App-defined event (the ``data.event`` field).
payload: App-defined payload (the ``data.payload`` field).
"""
msg_id: str = ""
event_name: str = ""
event: str = ""
payload: Any = None
def __str__(self):
"""String representation of the UI event frame."""
return f"{self.name}(event: {self.event_name})"
return f"{self.name}(event: {self.event})"
@dataclass

View File

@@ -595,11 +595,11 @@ class UIEventData(BaseModel):
"""Inner ``data`` for a ``ui-event`` message.
Parameters:
name: App-defined event name.
event: App-defined event.
payload: App-defined payload, schemaless by design.
"""
name: str
event: str
payload: Any | None = None
@@ -607,13 +607,13 @@ class UICommandData(BaseModel):
"""Inner ``data`` for a ``ui-command`` message.
Parameters:
name: App-defined command name.
command: App-defined command.
payload: App-defined payload (already a plain dict by the
time it lands on the wire). The standard payload models
time it lands on the wire). The standard command payload models
below produce the right shape via ``model_dump()``.
"""
name: str
command: str
payload: Any | None = None

View File

@@ -434,7 +434,7 @@ class RTVIObserver(BaseObserver):
await self.send_rtvi_message(message)
elif isinstance(frame, RTVIUICommandFrame):
message = RTVI.UICommandMessage(
data=RTVI.UICommandData(name=frame.command_name, payload=frame.payload)
data=RTVI.UICommandData(command=frame.command, payload=frame.payload)
)
await self.send_rtvi_message(message)
elif isinstance(frame, RTVIUITaskFrame):

View File

@@ -299,7 +299,7 @@ class RTVIProcessor(FrameProcessor):
await self.push_frame(
RTVIUIEventFrame(
msg_id=message.id,
event_name=event_data.name,
event=event_data.event,
payload=event_data.payload,
)
)

View File

@@ -14,6 +14,10 @@ with existing client code, and we want a test that fails loudly.
import unittest
from pipecat.processors.frameworks.rtvi.frames import (
RTVIUICommandFrame,
RTVIUIEventFrame,
)
from pipecat.processors.frameworks.rtvi.models import (
Click,
Focus,
@@ -43,27 +47,27 @@ class TestEnvelopeMessages(unittest.TestCase):
"""Pin the on-the-wire envelope shapes for each first-class UI message."""
def test_ui_event_envelope(self):
msg = UIEventMessage(id="m1", data=UIEventData(name="nav_click", payload={"view": "home"}))
msg = UIEventMessage(id="m1", data=UIEventData(event="nav_click", payload={"view": "home"}))
self.assertEqual(
msg.model_dump(),
{
"label": "rtvi-ai",
"type": "ui-event",
"id": "m1",
"data": {"name": "nav_click", "payload": {"view": "home"}},
"data": {"event": "nav_click", "payload": {"view": "home"}},
},
)
def test_ui_command_envelope_no_id(self):
# Server-to-client push: no id field on the envelope (matches
# ServerMessage / LLMFunctionCallMessage shape).
msg = UICommandMessage(data=UICommandData(name="toast", payload={"title": "Saved"}))
msg = UICommandMessage(data=UICommandData(command="toast", payload={"title": "Saved"}))
self.assertEqual(
msg.model_dump(),
{
"label": "rtvi-ai",
"type": "ui-command",
"data": {"name": "toast", "payload": {"title": "Saved"}},
"data": {"command": "toast", "payload": {"title": "Saved"}},
},
)
@@ -187,5 +191,20 @@ class TestPayloadShapes(unittest.TestCase):
self.assertIsNone(payload["end_offset"])
class TestFrames(unittest.TestCase):
"""Pin the frame API for named UI messages."""
def test_ui_command_frame_names_command_and_payload(self):
frame = RTVIUICommandFrame(command="toast", payload={"title": "Saved"})
self.assertEqual(frame.command, "toast")
self.assertEqual(frame.payload, {"title": "Saved"})
def test_ui_event_frame_names_event_and_payload(self):
frame = RTVIUIEventFrame(msg_id="m1", event="nav_click", payload={"view": "home"})
self.assertEqual(frame.msg_id, "m1")
self.assertEqual(frame.event, "nav_click")
self.assertEqual(frame.payload, {"view": "home"})
if __name__ == "__main__":
unittest.main()