From 41124dc494ee7016a0559aac0998bbba2c6798df Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Wed, 6 May 2026 11:08:25 -0400 Subject: [PATCH] refactor(rtvi): clarify UI message names --- .../processors/frameworks/rtvi/frames.py | 20 +++++++------- .../processors/frameworks/rtvi/models.py | 10 +++---- .../processors/frameworks/rtvi/observer.py | 2 +- .../processors/frameworks/rtvi/processor.py | 2 +- tests/test_rtvi_ui.py | 27 ++++++++++++++++--- 5 files changed, 39 insertions(+), 22 deletions(-) diff --git a/src/pipecat/processors/frameworks/rtvi/frames.py b/src/pipecat/processors/frameworks/rtvi/frames.py index e44de7f1d..30d994875 100644 --- a/src/pipecat/processors/frameworks/rtvi/frames.py +++ b/src/pipecat/processors/frameworks/rtvi/frames.py @@ -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 diff --git a/src/pipecat/processors/frameworks/rtvi/models.py b/src/pipecat/processors/frameworks/rtvi/models.py index d814fa3e8..5840ecbd6 100644 --- a/src/pipecat/processors/frameworks/rtvi/models.py +++ b/src/pipecat/processors/frameworks/rtvi/models.py @@ -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 diff --git a/src/pipecat/processors/frameworks/rtvi/observer.py b/src/pipecat/processors/frameworks/rtvi/observer.py index f6cdfb45b..9f7bb09c6 100644 --- a/src/pipecat/processors/frameworks/rtvi/observer.py +++ b/src/pipecat/processors/frameworks/rtvi/observer.py @@ -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): diff --git a/src/pipecat/processors/frameworks/rtvi/processor.py b/src/pipecat/processors/frameworks/rtvi/processor.py index 0925106c8..80763c498 100644 --- a/src/pipecat/processors/frameworks/rtvi/processor.py +++ b/src/pipecat/processors/frameworks/rtvi/processor.py @@ -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, ) ) diff --git a/tests/test_rtvi_ui.py b/tests/test_rtvi_ui.py index adbfe9466..ec9e1247f 100644 --- a/tests/test_rtvi_ui.py +++ b/tests/test_rtvi_ui.py @@ -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()