Skip to content
RasaGet a free licence
Guides for AI teams

Guide · AI product engineer

Connect a LiveKit voice room to Rasa

Use LiveKit for the voice connection and speech services, and Rasa for dialogue, while checking messages after a participant reconnects.

by Rod Rivera

About 4 minutes

Key takeaways (3)
  • Retain your LiveKit room and speech services when adding Rasa dialogue.
  • Check the current participant connection when a delayed message finishes reading.
  • Test reconnects as well as changes of participant.

You already have a LiveKit voice room and speech services. This tutorial adds Rasa for dialogue and checks what happens when a participant disconnects while an incoming message is still being read.

Our test accepts text from the linked participant, pauses the reader, and lets another connection take over. The old text finishes reading afterwards. A bare callback still delivers it to the session; checking the current connection rejects it.

The native RoomIO controls exercise actual text, disconnect and participant-link handlers. They deliberately retain the session across the handoff. Synthetic readers, participants and a session-output boundary make that sequence inspectable:

Connection after handoffCompletion-time checkOld text deliveredOld text discarded
Different participantAbsentYesNo
Different participantPresentNoYes
New connection with the same identityAbsentYesNo
New connection with the same identityPresentNoYes

A current-connection control also delivers usable text. This is an application callback exercise, with retained-session behaviour chosen by the fixture. Accept the actual room transport, speech providers and acoustic interruption in your combined bot separately.

Keep the room and give Rasa the dialogue

Your existing LiveKit bot keeps its room, speech recognition, speech synthesis and turn detector. A native model adapter calls your Rasa REST conversation. Configure Rasa to collect missing information and call its domain tools. Configure LiveKit to carry the conversation through your existing media services.

The fictional Juniper Mobile task asks whether the caller means north or central, then explains a synthetic public maintenance record. Both this lesson and the Pipecat composition reuse the same small Rasa project. Create one selected dialogue environment when you need it, rather than another application for each media framework.

Existing LiveKit room Retained speech services and turn detector Incoming text reader Existing native agent session Still the configured participant connection? Discard old connection text No Yes Native Rasa dialogue adapter Rasa conversation and public lookup Configured room plus participant Conversation binding Complete reply to existing speech path
  1. Incoming text is checked after reading, and again after awaiting session interruption. A delayed reader holds the earlier participant object.
  2. The configured room and participant select the public Rasa conversation. Recreating a native model with the same binding retains that conversation.
FigureBind the conversation and check the incoming connection

The composition control constructs an actual AgentSession and checks that its supplied speech objects and turn detector remain the same. It also checks the selected participant and normal close-on-disconnect option. Starting a connected session with provider services is the reader’s next acceptance step.

Check the connection after the wait

LiveKit’s text input documentation says to “provide a text input callback function in room options”. Our native control checks what happens when a reader finishes after its connection changes. In the pinned SDK, the native text handler checks the linked participant, captures its participant object, then awaits the reader. It delivers a text event to the callback after that read completes.

Our completion check uses the configured room and participant, then checks whether the current room mapping still holds that exact participant object:

Check ownership on both sides of interruption
async def __call__(self,session,event):
    if not self.owns(event): self.discarded+=1;return
    if not event.text.strip() or len(event.text)>1000: self.discarded+=1;return
    await session.interrupt()
    if not self.owns(event): self.discarded+=1;return
    session.generate_reply(user_input=event.text)
  1. owns checks the configured room and participant, including whether the room still holds the captured participant object.
  2. Awaiting interruption gives the connection another opportunity to change.
  3. Repeat the ownership check before asking the retained session to generate a reply.

This is the pinned callback with explanatory markers added. Its ownership comparison is self.room.remote_participants.get(participant.identity) is participant.

That final comparison distinguishes the synthetic reconnect using the same identity from the original connection. The callback checks ownership before awaiting interruption and again afterwards. A separate native control replaces the connection during that await; old text is still discarded.

Keep the lifecycle explicit. The supplied bot recipe retains LiveKit’s normal close-on-disconnect policy. Start a new session and adapter for a different participant. The fixture keeps its session across the handoff to test the callback; carrying a customer’s old dialogue binding into a replacement session requires a deliberate identity and continuity policy.

Keep conversation state separate from connection state

The native stream control collects two valid scripted replies through actual LLMStream instances. Two rooms reuse the same public participant name. The inspectable synthetic server boundary records their previous messages:

Public conversation bindingSecond room receives first room’s historyNative reply text
Room and participantNoSynthetic reply.
Room omittedYesSynthetic reply.

The mutation omits only the room from the configured sender derivation. Reply shape stays the same while the synthetic server state changes. These are public fixture conversations. The same correct room/participant binding across fresh model objects retains the intended conversation.

The actual loopback HTTP control checks the configured sender on the received POST and collects the resulting native stream text. Only the final plain-text user message is sent. An earlier question is not replayed when the native context ends in a tool result, assistant message or unusable input.

Run the native controls, then your existing bot

Use Python 3.12. From a fresh checkout:

git clone https://github.com/RasaHQ/rasa-community-resources.git
cd rasa-community-resources
git fetch origin fbe31f9c9c930a3336b368dbf7e793cf31cf23e4
git checkout fbe31f9c9c930a3336b368dbf7e793cf31cf23e4
python3 scripts/workspace.py start tutorials/frameworks-with-rasa/livekit --owner juniper-room-demo
cd tutorials/frameworks-with-rasa/livekit

Run the controls before inspecting the demo:

Any system
make test
Any system
make demo

The recorded native suite completes successfully:

Ran 21 tests in 0.044s

OK

The qualification record binds source files and dated logs, including an earlier failing context test. That failure exposed replay of the previous user question after a tool result. The fix requires the final native context item itself to be a user message; the original negative assertion remains.

From the LiveKit project directory:

.venv/bin/python -m unittest discover -s tests -p 'test_native_room_input.py' -v
.venv/bin/python demo_late_text.py

Inspect both handoff types. The guarded observations discard old text, while the positive current-connection control delivers the current question. The additional control checks a connection change during awaited interruption.

Use an existing suitable Rasa REST endpoint, or follow the public maintenance project’s setup. Its decorated tools, validation, licensed training and cold archive loading are separately qualified in the pinned Rasa qualification. If Mantle is new to you, start with the Rasa quickstart. A Pipecat environment is unnecessary for this bot.

With that Rasa server running, from the LiveKit project:

.venv/bin/python run_text_demo.py --room public-juniper-room --participant public-caller --message 'Is maintenance planned for north?'
.venv/bin/python run_text_demo.py --room public-juniper-room --participant public-caller --message 'And for central?'

Keep the room and participant labels stable for those two questions. Inspect the returned wording against public records JM-N-101 and JM-C-102, then exercise missing-area and retry behaviour. These reader commands call your configured model. The native controls use scripted responses and zero model provider requests.

The complete bot composition example shows participant selection, retained speech objects, room options, session startup and local cleanup. Preserve your provider extras, transport setup and worker entrypoint. Qualify the combined lock against those services.

Keep complete replies and choose a retry policy

The native stream supplies no token usage. Collect actual model usage on the Rasa side; identify the actual model requests behind the media interface. A cancelled local stream cancels the pending local request. Accepted remote work needs its own cancellation and recovery contract.

After your own bot/server and requests finish, keep needed state, models and recordings. Retire only the selected environments you created using the companion retirement instructions.