fresh-television-67623
03/19/2026, 10:06 AMtall-airline-16756
03/19/2026, 10:06 AMtall-airline-16756
03/19/2026, 10:06 AMllm_node (or equivalent hook that runs before the agent call), scan chat history and:
◦ Replace any assistant messages that are not English with their original English versions (or re-translate them to English).
◦ Optionally drop older assistant messages if they are not needed for coherence.
• Example pattern:
# Pseudocode
for item in chat_ctx.items:
if item.role == "assistant" and not is_english(item.text):
item.content = [translate_to_english(item.text)]
3) Re-inject a strict system instruction each turn
• At the start of every turn inject a system message that explicitly mandates the assistant must reply only in English, regardless of user language.
• Make it concise and explicit, e.g.:
◦ "Always respond only in English. Do not produce replies in any other language. When the user speaks another language, translate the user content to English for understanding, respond in English, then provide the translated version downstream for TTS."
• Re-adding or resetting this system message each turn reduces instruction forgetting.
4) Anchor with examples and explicit prompt structure
• Put a short pattern/example in the system prompt showing user input in another language and the assistant reply in English:
◦ "User (Spanish): ¿Cómo estás?
Assistant (English): I'm doing well."
• Example-based anchoring helps the model keep format across turns.
5) Use turn-level enforcement and checks after generation
• After the LLM outputs, check language; if it’s not English, either:
◦ Reject and re-call the LLM with the same (or stronger) system message, or
◦ Post-process: translate the LLM output back to English and replace the assistant message in context with that English text (but re-calling is cleaner).
6) Reduce context length or prune old multilingual content
• If the conversation gets long and contains many translated messages, prune or summarize older turns into English-only summaries to preserve context without non-English content.
Which approach to start with
• If you can change the pipeline easily: implement option 1 (replace messages) + option 3 (re-inject system instruction each turn). That combination is the most robust.
• If you can hook into the LLM node: add a pre-call cleanup (option 2) to enforce English on any assistant entries.
• If you can only tweak prompts: strengthen the system message and add examples (option 4) and post-check outputs (option 5).
Two quick diagnostics to help me give targeted code examples
1. Are you currently replacing the user’s original (non-English) message in the ChatContext with the translated English version before the LLM call? (Yes/No)
2. Are you using a custom llm_node or pre-call hook where you can mutate the ChatContext before the model is invoked? (Yes/No)
If you share those answers I’ll give a concrete code snippet that fits your setup (llm_node hook or prompt-only approach).
Sources: Pipeline nodes and hooks | LiveKit Documentation | Agents and handoffs | LiveKit Documentation, with interactive elementsfresh-television-67623
03/19/2026, 11:09 AMtall-airline-16756
03/19/2026, 11:09 AMuser_turn_completed, and
• translate in llm_node because if you translate in tts_node the transcription shown to the user becomes English.
Goal recap: keep the chat context the LLM sees entirely in English, but show the user the transcription in their original language. Doable — translate at the STT step and keep the original transcription for display.
Recommended approach (concise plan)
1. Translate inside stt_node (or at STT time)
◦ Have STT produce the original-language transcript for frontend display.
◦ Immediately produce a translated-to-English copy and hand that English text to the chat context/LLM.
◦ Do not pass the original-language text into the chat context.
1. Keep transcription shown to the user in the original language
◦ Use the original STT transcript (pre-translation) for the transcription UI or for transcription_node.
◦ Only replace the LLM-visible message with the translated English variant.
1. Continue reinforcing English with a system message each turn
◦ Re-inject or refresh a short system instruction each turn enforcing “Respond only in English.”
◦ This plus English-only chat context prevents drift.
Why this order works
• stt_node controls what the LLM sees; translating there guarantees the chat history stays English.
• transcription_node (or your frontend) can still display the original-language transcript to the user, so UX is unchanged.
• Translating in llm_node lets non-English text slip into the context before translation; that causes the drift you’re seeing.
Practical implementation sketch
• In `stt_node`:
a. Get STT result (original_text).
b. Send original_text to frontend/transcription stream.
c. Translate original_text -> english_text.
d. Create an STT event (or message) whose .text that the LLM will see is english_text.
e. Yield that event into the pipeline so the LLM receives English.
Example pseudocode:
async def stt_node(self, audio, model_settings):
async for event in default_stt(audio, model_settings):
original_text = event.alternatives[0].text # e.g., Spanish
# 1) Send original to frontend/display (preserve it)
send_to_frontend_transcription(original_text)
# 2) Translate for LLM
english_text = await translate_to_english(original_text)
# 3) Replace event content for the LLM branch ONLY
event_for_llm = copy(event)
event_for_llm.alternatives[0].text = english_text
yield event_for_llm # LLM sees English
Notes and safeguards
• Keep a mapping/metadata linking the displayed original_text to the english_text if you need to correlate UI items with LLM turns.
• After the LLM responds in English, store only that English assistant message in the chat context (do not replace it with a translated-to-other-language version).
• Post-check: after generation, verify assistant output is English. If not, either re-run with a stronger system instruction or translate the output back to English and store the corrected English version in context.
• If you cannot modify stt_node directly, you can instead:
◦ Intercept STT output before adding it to chat context, store original for display, and add only the translated text to chat context.
◦ But modifying stt_node is cleaner.
Answering your exact constraint (why translating in tts_node caused wrong transcription)
• Translating in tts_node changes what you present as the transcription stream (because TTS sits after the LLM output path). That’s why the transcription showed English. Translating earlier (STT) lets you show original language while sending English to the model.
Quick questions so I can give code tailored to your setup
1. Can you modify stt_node directly in your pipeline? (Yes/No)
2. Which STT provider are you using (native STT, Gladia, etc.)? Some providers return both original and translated text which simplifies this.
3. Are you storing chat history centrally or streaming events to the LLM each turn? (Helps me show the exact place to replace messages.)
If you say “Yes” to 1, I’ll provide a ready-to-drop-in stt_node snippet for your runtime. If not, tell me your provider and how you currently wire STT -> LLM -> transcription UI and I’ll give the next-best integration approach.
Sources: Pipeline nodes and hooks | LiveKit Documentation | Text and transcriptions | LiveKit Documentation | Gladia STT plugin guide | LiveKit Documentation | TTS Translator with Gladia STT | LiveKit Documentation, with interactive elementsfresh-television-67623
03/19/2026, 11:17 AMtall-airline-16756
03/19/2026, 11:18 AMstt_node? (Yes/No) 2) Which STT provider are you using? (native, Gladia, other) 3) Do you currently store the assistant message in the chat context immediately after LLM generation, or do you mutate it later? (store immediately / mutate later)
Tell me those three answers and I’ll give a targeted snippet you can drop into your pipeline.
, with interactive elementsfresh-television-67623
03/19/2026, 11:22 AMtall-airline-16756
03/19/2026, 11:23 AMstt_node, llm_node (minimal), and the post-LLM translation + TTS step. Adapt variable names and integrations to your codebase.
# Helper: Sarvam translate function (awaitable)
async def sarvam_translate(text: str, target_lang: str) -> str:
# replace with your Sarvam call
return await sarvam_client.translate(text, target=target_lang)
# 1) STT node: send original transcript to frontend, feed English to LLM
async def stt_node(audio_stream, model_settings):
async for stt_event in default_stt(audio_stream, model_settings):
original_text = stt_event.alternatives[0].text # user language
user_lang = detect_language(original_text) # optional, or pass in session
# 1a. Send original_text to frontend transcription UI
send_to_frontend_transcription(original_text, lang=user_lang)
# 1b. Produce English text for LLM input (if user language != en)
if user_lang != "en":
english_user_text = await sarvam_translate(original_text, target_lang="en")
else:
english_user_text = original_text
# 1c. Yield an STT event whose text is the English text so the LLM sees English
event_for_llm = copy_of(stt_event)
event_for_llm.alternatives[0].text = english_user_text
yield event_for_llm
# 2) LLM node: ensure system prompt enforces English and store only English assistant output
async def llm_node(chat_ctx, tools, model_settings):
# Ensure a short system message exists each turn
enforce_system = {
"role": "system",
"content": "Always respond only in English. Do not output any other language."
}
# Prepend or update system message for the model call
chat_ctx.prepend_system_message(enforce_system)
# Call default LLM processing (streamed or not)
async for chunk in Agent.default.llm_node(chat_ctx, tools, model_settings):
yield chunk
# After generation: assume final assistant_text is available
assistant_english_text = chat_ctx.latest_assistant_text # or capture from result
# Persist only the English assistant message into chat context
chat_ctx.add_message(role="assistant", content=[assistant_english_text])
# 3) Translate assistant reply for user-facing output and send to TTS/frontend
user_lang = chat_ctx.session.user_language # obtain from session or detection
if user_lang != "en":
translated_for_user = await sarvam_translate(assistant_english_text, target_lang=user_lang)
else:
translated_for_user = assistant_english_text
# Send translated text to frontend (visible) and to TTS node
send_to_frontend_assistant_reply(translated_for_user, lang=user_lang)
send_to_tts_node(translated_for_user, lang=user_lang)
Quick notes and tips
• Do NOT replace the assistant English text in chat_ctx with the translated version. Always store the English assistant message.
• Keep a mapping (metadata) linking the stored English assistant message to the translated text shown to the user if you need to correlate events.
• If your STT already returns language metadata or both original+translated fields, use original for display and translated-for-LLM for chat ctx.
• Add a post-generation language check: if the LLM ever outputs non-English, re-call the model with a stronger system message or translate its output back to English and store that corrected English in context.
If you want, I can convert this to a drop-in snippet for your exact framework (show me the function signatures you currently use for stt_node, llm_node, and your TTS sender).
, with interactive elements