dazzling-truck-32153
12/23/2025, 8:36 PMtall-airline-16756
12/23/2025, 8:36 PMtall-airline-16756
12/23/2025, 8:37 PMgenerate_reply(instructions="") results in the function name (or the call) being spoken aloud.
Short answer:
• Passing an empty instructions string gives the model no guidance, so it can echo or hallucinate whatever context it finds — in your case it’s outputting the function-like text which then gets spoken by the TTS layer.
What you can do to fix it:
1. Provide a clear natural-language prompt in instructions, e.g. instructions="Greet the user and answer their question briefly."
2. Avoid putting raw code or function syntax inside `instructions`; the model may treat that as content to repeat.
3. If you need to speak an exact literal phrase, send it to the TTS/speech API directly (for example use the session say/play mechanism) rather than relying on the generative LLM to reproduce it.
If you want, share the exact call and resulting output you see and I’ll suggest the exact instructions wording to prevent the echo.
Sources: Agent speech and audio | LiveKit docsdazzling-truck-32153
12/23/2025, 8:39 PMtall-airline-16756
12/23/2025, 8:39 PMinstructions. I can help troubleshoot, but I need a bit more detail to pinpoint the cause.
Please share:
• The exact code snippet where you call generate_reply(...) and how you pass its result to TTS (copy/paste is best).
• An example instructions string you used that still produced the function name.
• Which SDK, agent, or runtime you’re using (browser JS, Node, Python, or a specific agent wrapper) and whether you’re using a separate TTS call or an automatic speech pipeline.
Quick checks you can try right away:
• Make sure you are not accidentally including the function call or debug text in the output path (for example, logging the call into the response text).
• Try a simple explicit instruction like instructions="Say: Hello world" and show me both the generated text and the spoken result.
• If you have a separate TTS step, try sending a hardcoded string to TTS (bypass the LLM) to confirm the TTS layer isn’t echoing the call.
Send the code and one failing example and I’ll analyze it and give an exact fix.dazzling-truck-32153
12/23/2025, 8:40 PMtall-airline-16756
12/23/2025, 8:40 PM@function_tool because the tool's return value is fed back into the LLM and can become part of the generated/speech output. In on_enter no tool return value is injected, so you don't see the echo.
What to do:
• If you call generate_reply(...) inside the tool, make the tool return None (or None, "log message") so nothing extra is sent back to the LLM.
Example (Python):
@function_tool
async def my_tool(self, context: RunContext):
await context.session.generate_reply(instructions="Say hello to the user")
return None # prevents the tool return value from being interpreted/spoken
If that doesn't fix it, please paste the exact tool function (how you call generate_reply) and the tool's return statement and I’ll pinpoint the change.
Sources: Tool definition and use | LiveKit docs | Pipeline nodes and hooks | LiveKit docs | SIP Lifecycle Management Agent | LiveKit docsdazzling-truck-32153
12/23/2025, 9:25 PMtall-airline-16756
12/23/2025, 9:25 PM@function_tool does:
• Exposes the function to the LLM as a callable tool the agent can invoke at runtime.
• Includes metadata (name, description, arg types) so the LLM knows when/how to call it.
• Is selected and called asynchronously by the LLM based on user input.
• Sends the function’s return value back to the LLM (so returning a string can become part of the conversation/speech).
• Is intended for actions the model may decide to run (lookups, external APIs, agent handoffs).
What a regular function does:
• Is only callable from your code (not visible or callable by the LLM).
• Has no automatic metadata exposed to the LLM.
• Is used for internal logic, helpers, or synchronous tasks you control.
• Its return value stays in local code unless you explicitly pass it to the LLM or another component.
When to use each:
• Use @function_tool for capabilities you want the model to autonomously call (e.g., fetch data, perform actions).
• Use regular functions for internal helpers and logic you don’t want the LLM to invoke.
Quick tip related to your earlier issue:
• If you call generate_reply() inside a @function_tool, return None (or None, "log") so the tool doesn’t send an unintended string back to the LLM and get spoken. Sources: Tool definition and use | LiveKit docsdazzling-truck-32153
12/23/2025, 10:12 PMtall-airline-16756
12/23/2025, 10:12 PMfinalize_call code and that the "yes" path logs and transfers correctly but the "no" path neither logs nor records the user's "no".
Likely cause
• Your tool is declared to return str. Returning None from a @function_tool can cause the agent runtime to treat that as "no return value" and not send it back to the LLM in a way that continues the flow or triggers logging the way you expect. That can make the LLM not register the "no" outcome.
• The first branch returns an empty string "" (which becomes a string return), the yes branch returns whatever speak_to_representative() returns (a string), but the no branch returns None — this mismatch is the probable root.
What to do
• Return a string consistently from every branch. Return "" or a short status message when you want silence but still a string sent back.
• Alternatively, explicitly log before returning and ensure end_call doesn't swallow or asynchronously hide the log.
Suggested fix (minimal change)
@function_tool
async def finalize_call(self, speak_with_rep: bool = None) -> str:
<http://logger.info|logger.info>("✅ finalize_call started")
if speak_with_rep is None:
await self.session.say(
"Would you like to speak with someone in regards to your project?",
allow_interruptions=False,
)
return ""
elif speak_with_rep is True:
<http://logger.info|logger.info>("✅ User wants transfer")
return await self.speak_to_representative()
else: # speak_with_rep is False
<http://logger.info|logger.info>("⛔ User declined transfer")
await end_call(session=self.session)
return "" # return a string instead of None
If you still don’t see the "User declined transfer" log after this, tell me:
• How the tool is being invoked (what triggers the False path).
• The implementation of end_call (it may raise/exit early or mute logs).dazzling-truck-32153
12/23/2025, 10:13 PMtall-airline-16756
12/23/2025, 10:13 PM@function_tool
async def finalize_call(self, speak_with_rep: bool = None) -> str:
<http://logger.info|logger.info>("✅ finalize_call started")
if speak_with_rep is None:
await self.session.say(
"Would you like to speak with someone in regards to your project?",
allow_interruptions=False,
)
return "" # ask-question phase; return empty string
if speak_with_rep is True:
<http://logger.info|logger.info>("✅ User wants transfer")
result = await self.speak_to_representative()
return result or "transfer_initiated"
# speak_with_rep is False
<http://logger.info|logger.info>("⛔ User declined transfer")
await end_call(session=self.session)
return "user_declined" # return a string so the tool result is recorded
Also confirm:
• The caller/LLM is passing the boolean speak_with_rep (True/False) to this tool rather than a string.
• Your intent parsing maps user "no" -> False.
If you paste the actual tool invocation (how the LLM or caller calls finalize_call) I’ll tell you exactly how to map the user's utterance to the boolean.dazzling-truck-32153
12/23/2025, 10:15 PMtall-airline-16756
12/23/2025, 10:15 PMfinalize_call() from on_enter which correctly asks the question, but you must ensure the user's answer is passed back into finalize_call as the speak_with_rep boolean in a follow-up call. Steps to collect the response correctly:
1. On enter: only ask the question and return (no decision).
async def on_enter(self) -> None:
await self.finalize_call() # speak_with_rep is None -> asks question
return None
1. Ensure the agent/LLM maps the user's reply (yes/no) into a second call to the tool with the boolean argument. Example flow the LLM should perform:
• LLM sees user answer -> calls tool: finalize_call(speak_with_rep=True) or finalize_call(speak_with_rep=False)
1. Make finalize_call handle three states and always return a string (never None). Example:
@function_tool
async def finalize_call(self, speak_with_rep: bool = None) -> str:
<http://logger.info|logger.info>("✅ finalize_call started")
if speak_with_rep is None:
await self.session.say(
"Would you like to speak with someone in regards to your project?",
allow_interruptions=False,
)
return "" # asked question
if speak_with_rep is True:
<http://logger.info|logger.info>("✅ User wants transfer")
result = await self.speak_to_representative()
return result or "transfer_initiated"
# speak_with_rep is False
<http://logger.info|logger.info>("⛔ User declined transfer")
await end_call(session=self.session)
return "user_declined"
1. Verify the LLM/tool invocation:
• Confirm the model/tool-calling layer is configured to call finalize_call with the boolean, not pass a raw string.
• If you rely on intent parsing, ensure "no"/"nah"/"not now" map to False and "yes"/"yeah" map to True.
Quick checklist if it still fails:
• Do not return None from the tool (use a string).
• Confirm the second invocation occurs (inspect logs for the tool being called with True/False).
• Confirm end_call does not terminate the process before logging.
If you paste the actual follow-up tool invocation (how the LLM calls finalize_call after the user replies), I’ll tell you exactly how to map the utterances to True/False and where to adjust.tall-airline-16756
12/23/2025, 10:18 PMend_call runs you hear the tool/function name spoken. Here’s exactly why and how to stop it.
Why it’s happening
• The agent runtime sends a tool’s return value back to the LLM as text. If that return value is a non-empty string (or the LLM infers something from the tool call), the LLM may include it in the generated speech. If a tool returns something unexpected or the runtime stringifies a value, that string can be spoken by the TTS.
Exact fixes (pick one)
1) Suppress any tool return sent to the LLM
• Make end_call return None so nothing is sent back for the LLM to speak.
async def end_call(session: AgentSession, params_filler: bool = False):
print("☎️ Ending call")
await session.generate_reply(instructions="Thank you for calling Hue-Line. Someone will contact you soon.")
await hangup_call()
return None
2) If end_call is decorated as a @function_tool, ensure it returns None (not a string). Tool returns are forwarded to the LLM; returning None prevents extra LLM replies.
3) If you must return a value, return an empty string "" (consistent string returns avoid unexpected content), but returning None is preferred to suppress LLM output.
Additional recommendations
• Call session.generate_reply(...) or session.say(...) for the exact spoken text, then return None immediately after.
• Do not return the function name or any status string you don’t want spoken.
• If hangup_call() raises or ends the process early, ensure logging happens before calling it.
If you want, paste whether end_call is a @function_tool or a plain helper and the exact logs of the LLM/tool call — I’ll point to the one-line change you need.dazzling-truck-32153
12/23/2025, 10:20 PMtall-airline-16756
12/23/2025, 10:20 PMend_call returns None but the function name (or tool return) is still being spoken. Short answer and exact steps to fix it:
Why it still speaks
• When a function decorated with @function_tool is called, its return value (or the tool call itself) is sent back into the LLM context. The LLM can then generate speech that echoes that tool/text. Even if end_call returns None, the tool caller (your finalize_call or the runtime) may still produce a message/state that the LLM speaks.
Do this exact, minimal change to stop it:
1) Don’t let the LLM generate the final TTS text for end-of-call messages. Use session.say(...) (or session.generate_reply(...)) to speak the exact phrase, then ensure the tool returns a sentinel that won’t be spoken (preferably None) — and make the tool itself NOT be a @function_tool.
2) Convert end_call to a plain helper (no @function_tool) and ensure the tool that calls it returns a string consistently or returns None correctly. Example changes:
• Make end_call a plain helper:
# plain helper, not a @function_tool
async def end_call(session: AgentSession, params_filler: bool = False):
print("☎️ Ending call")
await session.say("Thank you for calling Hue-Line. Someone will contact you soon.")
await hangup_call()
return None
• Ensure the calling tool returns a string or None consistently. For finalize_call (which is the @function_tool), do this:
@function_tool
async def finalize_call(self, speak_with_rep: bool = None) -> str:
<http://logger.info|logger.info>("✅ finalize_call started")
if speak_with_rep is None:
await self.session.say(
"Would you like to speak with someone in regards to your project?",
allow_interruptions=False,
)
return "" # asked question
if speak_with_rep is True:
<http://logger.info|logger.info>("✅ User wants transfer")
await self.speak_to_representative()
return "transfer_initiated"
# speak_with_rep is False
<http://logger.info|logger.info>("⛔ User declined transfer")
await end_call(session=self.session) # plain helper call
return "" # return empty string (not None) so runtime doesn't inject unexpected text
3) If you must keep end_call as a @function_tool, then:
• Have it call session.say(...) for the spoken phrase and return None, "" (two-tuple) or None explicitly — but note tool returns still get stringified and can trigger LLM output; making it a plain helper is safer.
Verification steps
• Confirm finalize_call is the only @function_tool exposed to the model for this flow.
• Watch logs: you should see the session.say text being sent, then finalize_call returning "" (or transfer_initiated) — no additional LLM-generated speech should occur.
• If you still hear a function name, paste the tool call logs (the runtime’s tool-call entry and the LLM response) and I’ll point out the exact spot producing the spoken text.
If you want, paste the runtime tool-call log for the "no" path now and I’ll mark the single line you need to change.dazzling-truck-32153
12/23/2025, 10:21 PMtall-airline-16756
12/23/2025, 10:21 PMasync def on_enter(self) -> None:
await self.session.say("Would you like to speak with someone in regards to your project?", allow_interruptions=False)
return None
2) Expose only the decision handler as a tool that accepts a bool
@function_tool
async def finalize_call(self, speak_with_rep: bool = None) -> None:
<http://logger.info|logger.info>("✅ finalize_call started")
if speak_with_rep is None:
# defensive: if called without value, re-ask
await self.session.say("Would you like to speak with someone in regards to your project?", allow_interruptions=False)
return None
if speak_with_rep is True:
<http://logger.info|logger.info>("✅ User wants transfer")
await self.speak_to_representative()
return None
# speak_with_rep is False
<http://logger.info|logger.info>("⛔ User declined transfer")
await self.session.say("Okay, I won't transfer you. Thank you for calling.")
await end_call(session=self.session) # end_call can be a plain helper
return None
Key points to ensure:
• The LLM will map user "yes"/"no" -> True/False and call finalize_call(speak_with_rep=True/False) automatically if the tool signature and docstring are clear.
• Always return None from the @function_tool to suppress any extra LLM reply being converted into speech.
• Use session.say(...) (or session.generate_reply(...)) to speak exact phrases; do not rely on returning strings from the tool for speech.
• Keep end_call as a plain helper (not a @function_tool) that calls session.say(...) then hangs up and returns None.
If you still hear the function name after this, paste the runtime tool-call log showing the LLM's tool invocation and response and I’ll point to the exact offending line.dazzling-truck-32153
12/23/2025, 10:29 PMtall-airline-16756
12/23/2025, 10:29 PMgenerate_reply(), but here's exactly why it causes the function name (or other unexpected text) to be spoken and how to use it safely.
Why generate_reply can cause the function name to be spoken
• generate_reply() asks the LLM to produce text based on context. When used inside a @function_tool, the tool's return value (or any string placed into the conversation context) is sent back to the LLM. The LLM may then echo or narrate that tool call/return in its generated reply, which the TTS will speak.
• session.say() emits an explicit spoken utterance and does not rely on the LLM to produce the exact wording, so it avoids the LLM reinterpreting tool returns and speaking them.
How to keep using generate_reply() safely
• Use generate_reply() only to produce content you want the LLM to generate and speak.
• If you call generate_reply() inside a @function_tool, make sure the tool returns None (not a string). Returning a string causes that string to be injected back into the LLM context and can be echoed.
• If you want the LLM to generate a reply but avoid the LLM narrating the tool call itself, do this pattern:
1) Ask question via LLM (on_enter)
await self.session.generate_reply(instructions="Ask: 'Would you like to speak with a representative?'")
2) Let the LLM call your tool with a boolean:
@function_tool
async def finalize_call(self, speak_with_rep: bool = None) -> None:
if speak_with_rep is None:
# defensive: re-ask using generate_reply if you prefer LLM phrasing
await self.session.generate_reply(instructions="Please ask: 'Would you like to speak with a representative?'")
return None
if speak_with_rep:
# If you want LLM phrasing for the transfer message, generate it and send to TTS,
# but ensure the tool returns None so the LLM doesn't echo the tool
reply = await self.session.generate_reply(instructions="Tell user we're transferring them, briefly.")
# If generate_reply returns text, send it explicitly to TTS via session.say or session.play_tts
await self.session.say(reply)
await self.speak_to_representative()
return None
# user declined
decline_reply = await self.session.generate_reply(instructions="Acknowledge decline succinctly.")
await self.session.say(decline_reply)
await end_call(session=self.session)
return None
Key rules
• Always return None from @function_tool functions when you do not want the LLM to generate an additional reply from the tool return.
• If generate_reply() is used to create the phrase, explicitly send that phrase to TTS using session.say() (or session.play_tts). Do not rely on returning the generated string from the tool.
• Keep tool return types consistent (prefer None) to avoid the runtime stringifying returns and feeding them back into the LLM.
If you want, I can convert the snippet above into your exact flow (keeping LLM-generated phrasing but preventing echoes). Paste where you call the tool and I’ll produce the one-line change.