Streaming LLM responses
Build a behavior that streams an LLM response and sends paragraphs as separate chat messages in real time.
When the LLM node has Stream enabled, it does not wait for
the full response. Instead, it immediately returns a streamKey and begins
accumulating the response in the background. You can then read a snapshot of the
response so far using the Read LLM Stream node.
This allows you to process partial output while the model is still generating.
Prerequisites
This guide builds on the concepts from Getting Started, Expressions, Using Variables, and Control Flow.
Creating a streaming behavior
We create a simple behavior that sends messages to the user as they arrive from the LLM stream. We treat each paragraph in the LLM response as a separate message.
You can view the completed behavior at openrp/behavior-examples.
Step 1: Set up
Start by setting up the same foundation from the Getting Started guide: receive a chat message, load the chat data, and identify which character should respond.
-
Add a Chat Message event node (
chatMessage). -
Connect a Get Chat node (
getChat).- Chat ID:
chatMessage.chatId - Expand Relations: select
participantsandmessages.
- Chat ID:
-
Connect a Filter node (
filter).- List:
getChat.participants.data - Item Condition:
item.userId === null
We use this to keep only the AI characters in the chat.
- List:
-
Connect an LLM node (
llm).- Prompt:
{{getChat.messages.data[0].content}} - Model ID:
chatMessage.modelSettings.chatModelId - Stream:
true
Since Stream is enabled, the LLM node returns immediately with a
streamKeyinstead of blocking the execution. - Prompt:
-
Connect a Set Variable node (
setVariable).- Variables:
- Key:
paragraphsSent - Value:
0
- Key:
We will use this variable in Step 3, to track which paragraph we have sent so far.
- Variables:
Step 2: Poll the stream
We now create a loop that repeatedly reads the LLM stream until it is complete.
-
From
setVariable, connect a Repeat Until node (repeatUntil).- Expression:
!readLlmStream.isFinished
This is the loop that will repeatedly read the LLM stream until it is complete. Note that we haven't added
readLlmStreamnode yet, so it won't show up in auto-complete. - Expression:
-
From the
loopStartoutput ofrepeatUntil, connect a Wait node (wait).This adds a delay between polls. Without it, the loop would spin as fast as possible without giving the stream time to accumulate more text.
-
Connect a Read LLM Stream node (
readLlmStream).- Stream Key:
llm.streamKey
- Stream Key:
Step 3: Insert messages
We want to send messages to the chat as soon as they arrive. Let's break up the response into separate paragraphs, and send each one as a separate message.
To avoid sending the same message twice, we use the variable we defined in Step 1 to keep track of which paragraphs we have already sent.
-
From
readLlmStream, connect a String Split node (stringSplit).- Text:
readLlmStream.snapshot.choices[0].message.content || ""
This breaks down the LLM response into separate paragraphs, using the default double newline as the separator.
Note
The
|| ""handles early iterations where the snapshot may not have any content yet, in which casetextwould evaluate to an empty string"". - Text:
-
Connect an If node (
if).- Expression:
(stringSplit.array.length - (readLlmStream.isFinished ? 0 : 1)) > $variables.paragraphsSent
This expression checks whether the split array has more paragraphs than we've already sent. While the stream is still running, we skip the last segment (hence we minus 1) because it's incomplete - AI is still writing that paragraph. Once the stream finishes, we include it.
- Expression:
-
From the True output of
if, connect a Repeat Until node (repeatUntil2).- Expression: same as the If:
(stringSplit.array.length - (readLlmStream.isFinished ? 0 : 1)) > $variables.paragraphsSent
We will send all unsent paragraphs to the chat with this inner loop.
- Expression: same as the If:
-
From the
loopStartofrepeatUntil2, connect an Insert Chat Message node (insertChatMessage).- Chat ID:
getChat.id - Content:
{{stringSplit.array[$variables.paragraphsSent]}} - Chat Participant ID:
filter.list[0].id
This sends the unsent paragraph as a chat message from the AI character.
- Chat ID:
-
Connect a Set Variable node (
setVariable).- Variables:
- Key:
paragraphsSent - Value:
$variables.paragraphsSent + 1
- Key:
- Variables:
-
Connect a Wait node (
wait2). -
Connect
wait2back to theloopEndinput ofrepeatUntil2. -
From the
nextoutput ofrepeatUntil2, connect an End If node (endIf). -
From the False output of
if, connect to the sameendIfnode. -
Connect
endIfto theloopEndinput ofrepeatUntil(the outer loop).
Testing the behavior
If you did everything correctly, you should end up with something similar to openrp/behavior-examples.
Now you can follow the test your behavior guide to run your behavior.
Wrapping up
In this guide, we created a behavior that uses two loops to stream an LLM response as it is being generated. The outer loop polls the stream for the current snapshot, while the inner loop detects whether there are any new paragraphs and sends them to the chat.
Depending on your use case, you may extend this example to do other things, such as asking the LLM to output separate messages for separate chat participants, or stop generation early if we detect someone else sent a message to the chat with a Get Chat Messages node.