Repository: pipecat-ai/pipecat
Pull request #1804: add text chatbot example using small webrtc transport
Author: login: vipyne
Created: 2025-05-12T19:56:24Z | Merged: 2025-05-28T19:25:42Z

#### Please describe the changes in your PR. If it is addressing an issue, please reference that as well.

Text only chat bot example using small webrtc transport (_not_ Daily) and RTVI.  

~TODO: add `text_enabled` to transport params to guide UI on whether or not to display chat input.~
Used a `RTVIServerMessageFrame` to update the UI.

Requires: https://github.com/pipecat-ai/small-webrtc-prebuilt/pull/9

Test with pipecat example [here](https://github.com/pipecat-ai/pipecat/pull/1804).


Diff:
diff --git a/examples/foundational/41a-text-only-webrtc.py b/examples/foundational/41a-text-only-webrtc.py
new file mode 100644
index 0000000000..f27bcca76a
--- /dev/null
+++ b/examples/foundational/41a-text-only-webrtc.py
@@ -0,0 +1,162 @@
+#
+# Copyright (c) 2024–2025, Daily
+#
+# SPDX-License-Identifier: BSD 2-Clause License
+#
+
+import argparse
+import os
+
+import aiohttp
+from dotenv import load_dotenv
+from loguru import logger
+
+from pipecat.frames.frames import (
+    LLMMessagesAppendFrame,
+)
+from pipecat.pipeline.pipeline import Pipeline
+from pipecat.pipeline.runner import PipelineRunner
+from pipecat.pipeline.task import PipelineParams, PipelineTask
+from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
+from pipecat.processors.frameworks.rtvi import (
+    ActionResult,
+    RTVIAction,
+    RTVIActionArgument,
+    RTVIConfig,
+    RTVIObserver,
+    RTVIProcessor,
+    RTVIServerMessageFrame,
+)
+from pipecat.services.openai import OpenAIContextAggregatorPair
+from pipecat.services.openai.llm import OpenAILLMService
+from pipecat.transports.base_transport import BaseTransport, TransportParams
+from pipecat.transports.services.daily import DailyParams
+
+load_dotenv(override=True)
+
+
+# This is an example of a text-only chatbot using small webrtc tranport.
+# It uses the small webrtc transport prebuilt web UI.
+# https://github.com/pipecat-ai/small-webrtc-prebuilt
+
+
+def create_action_llm_append_to_messages(context_aggregator: OpenAIContextAggregatorPair):
+    async def action_llm_append_to_messages_handler(
+        rtvi: RTVIProcessor, service: str, arguments: dict[str, any]
+    ) -> ActionResult:
+        run_immediately = arguments["run_immediately"] if "run_immediately" in arguments else True
+        logger.info(f"run_immediately: {run_immediately}")
+        if run_immediately:
+            await rtvi.interrupt_bot()
+            # We just interrupted the bot so it should be fine to use the
+            # context directly instead of through frame.
+            if "messages" in arguments and arguments["messages"]:
+                frame = LLMMessagesAppendFrame(messages=arguments["messages"])
+                await rtvi.push_frame(frame)
+
+            frame = context_aggregator.user().get_context_frame()
+            await rtvi.push_frame(frame)
+        return True
+
+    action_llm_append_to_messages = RTVIAction(
+        service="llm",
+        action="append_to_messages",
+        result="bool",
+        arguments=[
+            RTVIActionArgument(name="messages", type="array"),
+            RTVIActionArgument(name="run_immediately", type="bool"),
+        ],
+        handler=action_llm_append_to_messages_handler,
+    )
+    return action_llm_append_to_messages
+
+
+# We store functions so objects (e.g. SileroVADAnalyzer) don't get
+# instantiated. The function will be called when the desired transport gets
+# selected.
+transport_params = {
+    "webrtc": lambda: TransportParams(),
+}
+
+
+async def run_example(transport: BaseTransport, _: argparse.Namespace, handle_sigint: bool):
+    logger.info(f"Starting bot")
+
+    # Create an HTTP session for API calls
+    async with aiohttp.ClientSession() as session:
+        llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
+
+        messages = [
+            {
+                "role": "system",
+                "content": "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Respond to what the user said in a creative and helpful way.",
+            },
+        ]
+
+        context = OpenAILLMContext(messages)
+        context_aggregator = llm.create_context_aggregator(context)
+
+        action_llm_append_to_messages = create_action_llm_append_to_messages(context_aggregator)
+        rtvi = RTVIProcessor(config=RTVIConfig(config=[]))
+        rtvi.register_action(action_llm_append_to_messages)
+
+        pipeline = Pipeline(
+            [
+                transport.input(),
+                rtvi,
+                context_aggregator.user(),
+                llm,
+                transport.output(),
+                context_aggregator.assistant(),
+            ]
+        )
+
+        task = PipelineTask(
+            pipeline,
+            params=PipelineParams(
+                allow_interruptions=True,
+                enable_metrics=True,
+            ),
+            observers=[RTVIObserver(rtvi)],
+        )
+
+        @rtvi.event_handler("on_client_ready")
+        async def on_client_ready(rtvi):
+            logger.info("Pipecat client ready.")
+            await rtvi.set_bot_ready()
+
+            # This block is frontend UI specific
+            # These messages are intended for small webrtc UI to only handle text
+            # https://github.com/pipecat-ai/small-webrtc-prebuilt
+            messages = {
+                "show_text_container": True,
+                "show_video_container": False,
+                "show_debug_container": False,
+            }
+            rtvi_frame = RTVIServerMessageFrame(data=messages)
+            await task.queue_frames([rtvi_frame])
+
+        @transport.event_handler("on_client_connected")
+        async def on_client_connected(transport, client):
+            logger.info(f"Client connected: {client}")
+            # Kick off the conversation.
+            await task.queue_frames([context_aggregator.user().get_context_frame()])
+
+        @transport.event_handler("on_client_disconnected")
+        async def on_client_disconnected(transport, client):
+            logger.info(f"Client disconnected")
+
+        @transport.event_handler("on_client_closed")
+        async def on_client_closed(transport, client):
+            logger.info(f"Client closed connection")
+            await task.cancel()
+
+        runner = PipelineRunner(handle_sigint=False)
+
+        await runner.run(task)
+
+
+if __name__ == "__main__":
+    from run import main
+
+    main(run_example, transport_params=transport_params)
diff --git a/examples/foundational/41b-text-and-audio-webrtc.py b/examples/foundational/41b-text-and-audio-webrtc.py
new file mode 100644
index 0000000000..1d7e4cfa5f
--- /dev/null
+++ b/examples/foundational/41b-text-and-audio-webrtc.py
@@ -0,0 +1,179 @@
+#
+# Copyright (c) 2024–2025, Daily
+#
+# SPDX-License-Identifier: BSD 2-Clause License
+#
+
+import argparse
+import os
+
+import aiohttp
+from dotenv import load_dotenv
+from loguru import logger
+
+from pipecat.audio.vad.silero import SileroVADAnalyzer
+from pipecat.frames.frames import (
+    LLMMessagesAppendFrame,
+)
+from pipecat.pipeline.pipeline import Pipeline
+from pipecat.pipeline.runner import PipelineRunner
+from pipecat.pipeline.task import PipelineParams, PipelineTask
+from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
+from pipecat.processors.frameworks.rtvi import (
+    ActionResult,
+    RTVIAction,
+    RTVIActionArgument,
+    RTVIConfig,
+    RTVIObserver,
+    RTVIProcessor,
+    RTVIServerMessageFrame,
+)
+from pipecat.services.cartesia.tts import CartesiaTTSService
+from pipecat.services.deepgram.stt import DeepgramSTTService
+from pipecat.services.openai import OpenAIContextAggregatorPair
+from pipecat.services.openai.llm import OpenAILLMService
+from pipecat.transports.base_transport import BaseTransport, TransportParams
+from pipecat.transports.services.daily import DailyParams
+
+load_dotenv(override=True)
+
+# This is an example of a chatbot in which a user can speak and/or type text to communicate with the bot.
+# It uses the small webrtc transport prebuilt web UI.
+# https://github.com/pipecat-ai/small-webrtc-prebuilt
+
+
+def create_action_llm_append_to_messages(context_aggregator: OpenAIContextAggregatorPair):
+    async def action_llm_append_to_messages_handler(
+        rtvi: RTVIProcessor, service: str, arguments: dict[str, any]
+    ) -> ActionResult:
+        run_immediately = arguments["run_immediately"] if "run_immediately" in arguments else True
+
+        if run_immediately:
+            await rtvi.interrupt_bot()
+
+            # We just interrupted the bot so it should be fine to use the
+            # context directly instead of through frame.
+            if "messages" in arguments and arguments["messages"]:
+                mess = arguments["messages"]
+                frame = LLMMessagesAppendFrame(messages=arguments["messages"])
+                await rtvi.push_frame(frame)
+
+        if run_immediately:
+            frame = context_aggregator.user().get_context_frame()
+            await rtvi.push_frame(frame)
+
+        return True
+
+    action_llm_append_to_messages = RTVIAction(
+        service="llm",
+        action="append_to_messages",
+        result="bool",
+        arguments=[
+            RTVIActionArgument(name="messages", type="array"),
+            RTVIActionArgument(name="run_immediately", type="bool"),
+        ],
+        handler=action_llm_append_to_messages_handler,
+    )
+    return action_llm_append_to_messages
+
+
+# We store functions so objects (e.g. SileroVADAnalyzer) don't get
+# instantiated. The function will be called when the desired transport gets
+# selected.
+transport_params = {
+    "webrtc": lambda: TransportParams(
+        audio_in_enabled=True,
+        audio_out_enabled=True,
+        vad_analyzer=SileroVADAnalyzer(),
+    ),
+}
+
+
+async def run_example(transport: BaseTransport, _: argparse.Namespace, handle_sigint: bool):
+    logger.info(f"Starting bot")
+
+    # Create an HTTP session for API calls
+    async with aiohttp.ClientSession() as session:
+        stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY"))
+
+        llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
+
+        tts = CartesiaTTSService(
+            api_key=os.getenv("CARTESIA_API_KEY"), voice_id="71a7ad14-091c-4e8e-a314-022ece01c121"
+        )
+
+        messages = [
+            {
+                "role": "system",
+                "content": "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Respond to what the user says in a creative and helpful way. Explain to the User they can speak or type text to communicate with you.",
+            },
+        ]
+
+        context = OpenAILLMContext(messages)
+        context_aggregator = llm.create_context_aggregator(context)
+
+        action_llm_append_to_messages = create_action_llm_append_to_messages(context_aggregator)
+        rtvi = RTVIProcessor(config=RTVIConfig(config=[]))
+        rtvi.register_action(action_llm_append_to_messages)
+
+        pipeline = Pipeline(
+            [
+                transport.input(),
+                rtvi,
+                stt,
+                context_aggregator.user(),
+                llm,
+                tts,
+                transport.output(),
+                context_aggregator.assistant(),
+            ]
+        )
+
+        task = PipelineTask(
+            pipeline,
+            params=PipelineParams(
+                allow_interruptions=True,
+                enable_metrics=True,
+            ),
+            observers=[RTVIObserver(rtvi)],
+        )
+
+        @rtvi.event_handler("on_client_ready")
+        async def on_client_ready(rtvi):
+            logger.info("Pipecat client ready.")
+            await rtvi.set_bot_ready()
+
+            # This block is frontend UI specific
+            # These messages are intended for small webrtc UI to only handle text
+            # https://github.com/pipecat-ai/small-webrtc-prebuilt
+            messages = {
+                "show_text_container": True,
+                "show_debug_container": False,
+            }
+            rtvi_frame = RTVIServerMessageFrame(data=messages)
+            await task.queue_frames([rtvi_frame])
+
+        @transport.event_handler("on_client_connected")
+        async def on_client_connected(transport, client):
+            logger.info(f"Client connected: {client}")
+            # Kick off the conversation.
+            await task.queue_frames([context_aggregator.user().get_context_frame()])
+
+        @transport.event_handler("on_client_disconnected")
+        async def on_client_disconnected(transport, client):
+            logger.info(f"Client disconnected")
+
+        @transport.event_handler("on_client_closed")
+        async def on_client_closed(transport, client):
+            logger.info(f"Client closed connection")
+            await task.cancel()
+
+        runner = PipelineRunner(handle_sigint=False)
+
+        await runner.run(task)
+
+
+if __name__ == "__main__":
+    from run import main
+
+    main(run_example, transport_params=transport_params)


Reviews:
user: login: mattieruth
body: This looks good. A couple comments:

1. Can we add an overview and more inline documentation? I wish our examples did a better job of explaining beyond just the code and the name of the example. What are we trying to show how to do here?

2. TBH WebRTC seems overkill for a text-only communication. It seems like if I were building a text only pipeline, I would be happy with a simple websocket connection. I know that for the ease of building this example, that didn't make sense since we don't have an rtvi client-side websocket transport, but i guess it gets back to the purpose of this example. If it's to show text-only, then we DO have python websocket transports we could use without RTVI. If RTVI is important, then we can stick with the SmallWebRTC approach but should probably consider an RTVI-capable client-side websocket transport.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T13:10:48Z
user: login: filipi87
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T14:19:52Z
user: login: mattieruth
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T15:24:52Z
user: login: filipi87
body: Looks great. I have added one note about the text-voice example, that I guess it would be nice to add.

And before we merge this, we need to first release the new version of `pipecat-ai-small-webrtc-prebuilt` with support for text. 🙂


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T14:17:38Z
user: login: filipi87
body: LGTM! 🚀
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T15:05:43Z

Review comments:
diff_hunk: @@ -0,0 +1,165 @@
+#
+# Copyright (c) 2024–2025, Daily
+#
+# SPDX-License-Identifier: BSD 2-Clause License
+#
+
+import argparse
+import asyncio
+import io
+import os
+import re
+import shutil
+import sys
+
+import aiohttp
+from dotenv import load_dotenv
+from loguru import logger
+from mcp import StdioServerParameters
+from PIL import Image
+
+from pipecat.audio.vad.silero import SileroVADAnalyzer
+from pipecat.frames.frames import (
+    Frame,
+    FunctionCallResultFrame,
+    LLMMessagesAppendFrame,
+    URLImageRawFrame,
+)
+from pipecat.pipeline.pipeline import Pipeline
+from pipecat.pipeline.runner import PipelineRunner
+from pipecat.pipeline.task import PipelineParams, PipelineTask
+from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
+from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
+from pipecat.processors.frameworks.rtvi import (
+    ActionResult,
+    RTVIAction,
+    RTVIActionArgument,
+    RTVIConfig,
+    RTVIObserver,
+    RTVIProcessor,
+    RTVIServerMessageFrame,
+)
+from pipecat.services.openai.llm import OpenAILLMService
+from pipecat.transports.base_transport import TransportParams
+from pipecat.transports.network.small_webrtc import SmallWebRTCTransport
+from pipecat.transports.network.webrtc_connection import SmallWebRTCConnection
+
+load_dotenv(override=True)
+
+
+async def run_bot(webrtc_connection: SmallWebRTCConnection, _: argparse.Namespace):
+    logger.info(f"Starting bot")
+
+    transport = SmallWebRTCTransport(
+        webrtc_connection=webrtc_connection,
+        params=TransportParams(),
+    )
+
+    # Create an HTTP session for API calls
+    async with aiohttp.ClientSession() as session:
+        llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
+
+        messages = [
+            {
+                "role": "system",
+                "content": "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Respond to what the user said in a creative and helpful way.",
+            },
+        ]
+
+        context = OpenAILLMContext(messages)
+        context_aggregator = llm.create_context_aggregator(context)
+
+        async def action_llm_append_to_messages_handler(
+            rtvi: RTVIProcessor, service: str, arguments: dict[str, any]
+        ) -> ActionResult:
+            run_immediately = (
+                arguments["run_immediately"] if "run_immediately" in arguments else True
+            )
+
+            if run_immediately:
+                await rtvi.interrupt_bot()
+
+            # We just interrupted the bot so it should be fine to use the
+            # context directly instead of through frame.
+            if "messages" in arguments and arguments["messages"]:
+                mess = arguments["messages"]
+                frame = LLMMessagesAppendFrame(messages=arguments["messages"])
+                await rtvi.push_frame(frame)
+
+            if run_immediately:
+                frame = context_aggregator.user().get_context_frame()
+                await rtvi.push_frame(frame)
+
+            return True
+
+        action_llm_append_to_messages = RTVIAction(
+            service="llm",
+            action="append_to_messages",
+            result="bool",
+            arguments=[
+                RTVIActionArgument(name="messages", type="array"),
+                RTVIActionArgument(name="run_immediately", type="bool"),
+            ],
+            handler=action_llm_append_to_messages_handler,
+        )
+
+        rtvi = RTVIProcessor(config=RTVIConfig(config=[]))
+        rtvi.register_action(action_llm_append_to_messages)
+
+        pipeline = Pipeline(
+            [
+                transport.input(),
+                rtvi,
+                context_aggregator.user(),
+                llm,
+                transport.output(),
+                context_aggregator.assistant(),
+            ]
+        )
+
+        task = PipelineTask(
+            pipeline,
+            params=PipelineParams(
+                allow_interruptions=True,
+                enable_metrics=True,
+            ),
+            observers=[RTVIObserver(rtvi)],
+        )
+
+        @rtvi.event_handler("on_client_ready")
+        async def on_client_ready(rtvi):
+            logger.info("Pipecat client ready.")
+            await rtvi.set_bot_ready()
+            # Update small webrtc UI to just handle text
path: examples/foundational/41-text-only-webrtc.py
user: login: mattieruth
body: what is small webrtc UI? Is this a Pipecat standard? Or are these custom messages we have introduced as part of this example to pair with an example client? Where is that client?
created_at: 2025-05-15T13:01:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,165 @@
+#
+# Copyright (c) 2024–2025, Daily
+#
+# SPDX-License-Identifier: BSD 2-Clause License
+#
+
+import argparse
+import asyncio
+import io
+import os
+import re
+import shutil
+import sys
+
+import aiohttp
+from dotenv import load_dotenv
+from loguru import logger
+from mcp import StdioServerParameters
+from PIL import Image
+
+from pipecat.audio.vad.silero import SileroVADAnalyzer
+from pipecat.frames.frames import (
+    Frame,
+    FunctionCallResultFrame,
+    LLMMessagesAppendFrame,
+    URLImageRawFrame,
+)
+from pipecat.pipeline.pipeline import Pipeline
+from pipecat.pipeline.runner import PipelineRunner
+from pipecat.pipeline.task import PipelineParams, PipelineTask
+from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
+from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
+from pipecat.processors.frameworks.rtvi import (
+    ActionResult,
+    RTVIAction,
+    RTVIActionArgument,
+    RTVIConfig,
+    RTVIObserver,
+    RTVIProcessor,
+    RTVIServerMessageFrame,
+)
+from pipecat.services.openai.llm import OpenAILLMService
+from pipecat.transports.base_transport import TransportParams
+from pipecat.transports.network.small_webrtc import SmallWebRTCTransport
+from pipecat.transports.network.webrtc_connection import SmallWebRTCConnection
+
+load_dotenv(override=True)
+
+
+async def run_bot(webrtc_connection: SmallWebRTCConnection, _: argparse.Namespace):
+    logger.info(f"Starting bot")
+
+    transport = SmallWebRTCTransport(
+        webrtc_connection=webrtc_connection,
+        params=TransportParams(),
+    )
+
+    # Create an HTTP session for API calls
+    async with aiohttp.ClientSession() as session:
+        llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
+
+        messages = [
+            {
+                "role": "system",
+                "content": "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Respond to what the user said in a creative and helpful way.",
+            },
+        ]
+
+        context = OpenAILLMContext(messages)
+        context_aggregator = llm.create_context_aggregator(context)
+
+        async def action_llm_append_to_messages_handler(
+            rtvi: RTVIProcessor, service: str, arguments: dict[str, any]
+        ) -> ActionResult:
+            run_immediately = (
+                arguments["run_immediately"] if "run_immediately" in arguments else True
+            )
+
+            if run_immediately:
+                await rtvi.interrupt_bot()
+
+            # We just interrupted the bot so it should be fine to use the
+            # context directly instead of through frame.
+            if "messages" in arguments and arguments["messages"]:
+                mess = arguments["messages"]
+                frame = LLMMessagesAppendFrame(messages=arguments["messages"])
+                await rtvi.push_frame(frame)
+
+            if run_immediately:
+                frame = context_aggregator.user().get_context_frame()
+                await rtvi.push_frame(frame)
+
+            return True
+
+        action_llm_append_to_messages = RTVIAction(
+            service="llm",
+            action="append_to_messages",
+            result="bool",
+            arguments=[
+                RTVIActionArgument(name="messages", type="array"),
+                RTVIActionArgument(name="run_immediately", type="bool"),
+            ],
+            handler=action_llm_append_to_messages_handler,
+        )
+
+        rtvi = RTVIProcessor(config=RTVIConfig(config=[]))
+        rtvi.register_action(action_llm_append_to_messages)
+
+        pipeline = Pipeline(
+            [
+                transport.input(),
+                rtvi,
+                context_aggregator.user(),
+                llm,
+                transport.output(),
+                context_aggregator.assistant(),
+            ]
+        )
+
+        task = PipelineTask(
+            pipeline,
+            params=PipelineParams(
+                allow_interruptions=True,
+                enable_metrics=True,
+            ),
+            observers=[RTVIObserver(rtvi)],
+        )
+
+        @rtvi.event_handler("on_client_ready")
+        async def on_client_ready(rtvi):
+            logger.info("Pipecat client ready.")
+            await rtvi.set_bot_ready()
+            # Update small webrtc UI to just handle text
path: examples/foundational/41-text-only-webrtc.py
user: login: filipi87
body: Hi @mattieruth , the small webrtc UI  is a simple, ready-to-use client for testing the `SmallWebRTCTransport`.
Like our Prebuilt for Daily.

We have created it only for making it easy testing the `SmallWebRTCTransport`.
- https://github.com/pipecat-ai/small-webrtc-prebuilt

You should also probably check this PR from Vanessa:
- https://github.com/pipecat-ai/small-webrtc-prebuilt/pull/9
created_at: 2025-05-19T14:19:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,165 @@
+#
+# Copyright (c) 2024–2025, Daily
+#
+# SPDX-License-Identifier: BSD 2-Clause License
+#
+
+import argparse
+import asyncio
+import io
+import os
+import re
+import shutil
+import sys
+
+import aiohttp
+from dotenv import load_dotenv
+from loguru import logger
+from mcp import StdioServerParameters
+from PIL import Image
+
+from pipecat.audio.vad.silero import SileroVADAnalyzer
+from pipecat.frames.frames import (
+    Frame,
+    FunctionCallResultFrame,
+    LLMMessagesAppendFrame,
+    URLImageRawFrame,
+)
+from pipecat.pipeline.pipeline import Pipeline
+from pipecat.pipeline.runner import PipelineRunner
+from pipecat.pipeline.task import PipelineParams, PipelineTask
+from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
+from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
+from pipecat.processors.frameworks.rtvi import (
+    ActionResult,
+    RTVIAction,
+    RTVIActionArgument,
+    RTVIConfig,
+    RTVIObserver,
+    RTVIProcessor,
+    RTVIServerMessageFrame,
+)
+from pipecat.services.openai.llm import OpenAILLMService
+from pipecat.transports.base_transport import TransportParams
+from pipecat.transports.network.small_webrtc import SmallWebRTCTransport
+from pipecat.transports.network.webrtc_connection import SmallWebRTCConnection
+
+load_dotenv(override=True)
+
+
+async def run_bot(webrtc_connection: SmallWebRTCConnection, _: argparse.Namespace):
+    logger.info(f"Starting bot")
+
+    transport = SmallWebRTCTransport(
+        webrtc_connection=webrtc_connection,
+        params=TransportParams(),
+    )
+
+    # Create an HTTP session for API calls
+    async with aiohttp.ClientSession() as session:
+        llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
+
+        messages = [
+            {
+                "role": "system",
+                "content": "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Respond to what the user said in a creative and helpful way.",
+            },
+        ]
+
+        context = OpenAILLMContext(messages)
+        context_aggregator = llm.create_context_aggregator(context)
+
+        async def action_llm_append_to_messages_handler(
+            rtvi: RTVIProcessor, service: str, arguments: dict[str, any]
+        ) -> ActionResult:
+            run_immediately = (
+                arguments["run_immediately"] if "run_immediately" in arguments else True
+            )
+
+            if run_immediately:
+                await rtvi.interrupt_bot()
+
+            # We just interrupted the bot so it should be fine to use the
+            # context directly instead of through frame.
+            if "messages" in arguments and arguments["messages"]:
+                mess = arguments["messages"]
+                frame = LLMMessagesAppendFrame(messages=arguments["messages"])
+                await rtvi.push_frame(frame)
+
+            if run_immediately:
+                frame = context_aggregator.user().get_context_frame()
+                await rtvi.push_frame(frame)
+
+            return True
+
+        action_llm_append_to_messages = RTVIAction(
+            service="llm",
+            action="append_to_messages",
+            result="bool",
+            arguments=[
+                RTVIActionArgument(name="messages", type="array"),
+                RTVIActionArgument(name="run_immediately", type="bool"),
+            ],
+            handler=action_llm_append_to_messages_handler,
+        )
+
+        rtvi = RTVIProcessor(config=RTVIConfig(config=[]))
+        rtvi.register_action(action_llm_append_to_messages)
+
+        pipeline = Pipeline(
+            [
+                transport.input(),
+                rtvi,
+                context_aggregator.user(),
+                llm,
+                transport.output(),
+                context_aggregator.assistant(),
+            ]
+        )
+
+        task = PipelineTask(
+            pipeline,
+            params=PipelineParams(
+                allow_interruptions=True,
+                enable_metrics=True,
+            ),
+            observers=[RTVIObserver(rtvi)],
+        )
+
+        @rtvi.event_handler("on_client_ready")
+        async def on_client_ready(rtvi):
+            logger.info("Pipecat client ready.")
+            await rtvi.set_bot_ready()
+            # Update small webrtc UI to just handle text
path: examples/foundational/41-text-only-webrtc.py
user: login: mattieruth
body: thanks. while i did want the answers to that question for myself, my bigger point was that we need documentation here to answer those questions for anyone coming to this example. the existing comment is not enough.
created_at: 2025-05-19T15:24:52Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-05-12T19:58:00Z
body: ## [Codecov](https://app.codecov.io/gh/pipecat-ai/pipecat/pull/1804?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pipecat-ai) Report
:white_check_mark: All modified and coverable lines are covered by tests.
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: vipyne
created_at: 2025-05-20T21:30:44Z
body: > 2. TBH WebRTC seems overkill for a text-only communication. It seems like if I were building a text only pipeline, I would be happy with a simple websocket connection. I know that for the ease of building this example, that didn't make sense since we don't have an rtvi client-side websocket transport, but i guess it gets back to the purpose of this example. If it's to show text-only, then we DO have python websocket transports we could use without RTVI. If RTVI is important, then we can stick with the SmallWebRTC approach but should probably consider an RTVI-capable client-side websocket transport.

I think an RTVI-capable client-side websocket transport sounds like a worthy pursuit. I'm hesitant to take that on right now with impending RTVI changes, but it makes sense to me that eventually there should be a plain jane websocket transport in [this list](https://github.com/pipecat-ai/pipecat-client-web-transports/tree/main/transports).

I think the aim here is to show text usage with the lowest lift from existing client-side libraries.  My assumption is folks using pipecat are using audio input in addition to text, which makes webrtc make sense as a transport.  This example aims to be simple by only showing text.  I will add another example that shows text and audio (and maybe even video).  

re:
> python websocket transports we could use without RTVI.
There is [this websocket example](https://github.com/pipecat-ai/pipecat/tree/main/examples/websocket-server) I considered as a base for a text chat bot example.  Maybe that should still be on the TODO list.  But we don't have many other examples using protobufs and I hesitate to build on that (at least not without putting more thought into it).


I agree this could use much more commentary. I'll add that as well.
author_association: MEMBER
user: login: vipyne
created_at: 2025-05-20T21:34:09Z
body: There is also [this text chatbot example](https://github.com/pipecat-ai/pipecat/pull/1608) that uses daily transport.  it's pretty heavy.  linked here for posterity, but I imagine it'll be moot soon.
author_association: MEMBER
user: login: filipi87
created_at: 2025-05-23T14:16:07Z
body: @vipyne, In the text-and-voice example: if I speak the user's part, the spoken input is not shown in the conversation. Only typed input appears. 
I think it would be helpful to display spoken user input as well, so we can clearly distinguish between the user's and the bot's turns.


author_association: CONTRIBUTOR
user: login: vipyne
created_at: 2025-05-26T16:29:01Z
body: [pipecat-ai-small-webrtc-prebuilt](https://github.com/pipecat-ai/small-webrtc-prebuilt/pull/12) released 👍 
author_association: MEMBER
user: login: vipyne
created_at: 2025-05-27T14:56:48Z
body: > @vipyne, In the text-and-voice example: if I speak the user's part, the spoken input is not shown in the conversation. Only typed input appears.
> I think it would be helpful to display spoken user input as well, so we can clearly distinguish between the user's and the bot's turns.

@filipi87 https://github.com/pipecat-ai/small-webrtc-prebuilt/pull/13  🎸 
author_association: MEMBER