Skip to main content

Text Chat

Send text messages to your AI character and handle streaming responses.

Sending Text

Use send_text() to send a message after connecting:

client.send_text("What is the weather like today?")

The method emits a text event over the WebSocket. The server processes the message through the AI pipeline and streams the response back via bot_response events.

info

send_text() raises EstuaryError with code NOT_CONNECTED if the client is not connected. Always call connect() first.

Receiving Responses

Bot responses arrive as a stream of chunks. Each chunk is a BotResponse dataclass:

async def on_response(response):
if response.is_final:
# Full response is ready
print("Complete response:", response.text)
else:
# Partial chunk
print(response.partial, end="", flush=True)

client.on("bot_response", on_response)

BotResponse Fields

FieldTypeDescription
textstrThe full accumulated response text so far
partialstrThe text content of just this chunk
is_finalboolTrue when the response is complete
message_idstrUnique identifier for this response
chunk_indexintSequential index of this chunk (starts at 0)
is_interjectionboolTrue if this is a proactive message, not a reply to user input

Streaming Pattern

A typical response arrives as multiple events:

bot_response { chunk_index: 0, partial: "The weather",  is_final: False, text: "The weather" }
bot_response { chunk_index: 1, partial: " today is", is_final: False, text: "The weather today is" }
bot_response { chunk_index: 2, partial: " sunny and", is_final: False, text: "The weather today is sunny and" }
bot_response { chunk_index: 3, partial: " warm.", is_final: True, text: "The weather today is sunny and warm." }

The text field accumulates across chunks, so on is_final: True it contains the full response.

Text-Only Mode

By default, sending text triggers both a text response (bot_response) and a voice response (bot_voice). To suppress the voice response and receive text only, pass text_only=True:

# Text response only -- no TTS audio generated
client.send_text("Give me a summary of our conversation.", text_only=True)

This is useful for programmatic interactions where voice output is not needed, or to reduce latency and bandwidth.

Interrupting a Response

You can interrupt an in-progress response with interrupt(). This tells the server to stop generating and clears any queued audio:

# Interrupt the current response
client.interrupt()

# Optionally specify which message to interrupt
client.interrupt(response.message_id)

Listen for the server's confirmation:

async def on_interrupt(data):
print("Response interrupted:", data.message_id)

client.on("interrupt", on_interrupt)

Scripted Lines

Use say_line() to make the character speak a specific prewritten line. The server skips the LLM entirely and feeds the text straight to TTS. Use this for cutscenes, branching dialogue, tutorials, or any moment where you want exact control over what the character says:

client.say_line("Welcome back, friend.")

say_line() interrupts any in-progress response before speaking the new line. The line is saved to chat history as a normal assistant turn, so the character's memory of the conversation stays consistent. Observe playback via the normal bot_response and bot_voice events.

Text-Only Scripted Lines

Pass text_only=True to inject the line into chat history without generating TTS audio. Useful for narrator-style interjections or when you want to feed context into the conversation without the character speaking out loud:

client.say_line("The door creaks open.", text_only=True)

Behavior

ConditionBehavior
Blank or whitespace-only textSilently ignored (no-op)
Called before connect()Raises EstuaryError with code NOT_CONNECTED
In-progress bot responseInterrupted before the new line is spoken
info

say_line() raises EstuaryError with code NOT_CONNECTED if the client is not connected. Always call connect() first.

Interjections

Sometimes the character sends a message without being prompted -- for example, a greeting when you first connect, or a follow-up question. These are marked with is_interjection:

async def on_response(response):
if response.is_interjection and response.is_final:
print("Character said (unprompted):", response.text)

client.on("bot_response", on_response)

Send and Wait

For simple request-response flows, send_text_and_wait() sends a message and returns the final BotResponse directly — no manual event listener needed:

async def send_text_and_wait(
text: str,
*,
text_only: bool = False,
timeout: float = 20.0,
) -> BotResponse
ParameterTypeDefaultDescription
textstrrequiredThe message text
text_onlyboolFalseIf True, suppress voice response
timeoutfloat20.0Max seconds to wait for a final response

Returns: The final BotResponse (with is_final=True).

Raises: asyncio.TimeoutError if no final response within timeout. EstuaryError with NOT_CONNECTED if not connected.

import asyncio
from estuary_sdk import EstuaryClient, EstuaryConfig

async def main():
async with EstuaryClient(config) as client:
await client.connect()

response = await client.send_text_and_wait("What is the capital of France?")
print(response.text) # "The capital of France is Paris."

# Text-only (no TTS), longer timeout
response = await client.send_text_and_wait(
"Summarize our conversation.",
text_only=True,
timeout=30.0,
)
print(response.text)

The listener is registered before sending and cleaned up automatically whether the call succeeds or times out. This replaces the manual pattern of registering a bot_response listener and waiting for is_final.

Example: Chat Loop

Here is a complete example of a text chat loop using aioconsole:

import asyncio
from estuary_sdk import EstuaryClient, EstuaryConfig

config = EstuaryConfig(
server_url="https://api.estuary-ai.com",
api_key="est_your_api_key",
character_id="your-character-uuid",
player_id="user-123",
)

async def main():
async with EstuaryClient(config) as client:
async def on_response(response):
if response.is_final:
print(f"\nBot: {response.text}\n")

client.on("bot_response", on_response)

await client.connect()
print("Connected! Type a message and press Enter.\n")

loop = asyncio.get_event_loop()
while True:
text = await loop.run_in_executor(None, input, "You: ")
if text.strip():
client.send_text(text.strip())

asyncio.run(main())

Next Steps