generate_reply(instructions="") reads out the func...
# ask-ai
d
generate_reply(instructions="") reads out the function name out loud...why?
t
Thanks for reaching out, Billy. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I understand you're asking why calling
generate_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 docs
d
It reads out the entire function name, does not matter what instructions I place.
t
Thanks — I hear your frustration that the function name is always being spoken no matter what you put in
instructions
. 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.
d
I just need to know why... on_enter it works fine... but when called from a @function_tool it reads out the entre function name
t
Thanks — that extra detail helps. You're seeing the function name spoken when called from a
@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):
Copy code
@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 docs
d
what is the diff between @function_tool and a regular function
t
Good question — here’s a concise comparison so you can see when to use each. What
@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 docs
d
@function_tool async def finalize_call(self, speak_with_rep: bool = None) -> str: """ Ask caller if they want to speak with someone. Args: speak_with_rep: True if user wants to speak with rep, False if they decline, None to ask the question """ logger.info("✅ finalize_call started") if speak_with_rep is None: # First call - ask the question 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: # User said yes logger.info("✅ User wants transfer") return await self.speak_to_representative() else: # _speak_with_rep is False_ # User said no logger.info("⛔ User declined transfer") await end_call(session=self.session) # ✅ Pass session too return None whenever user says no it does not collect it and log user declined but when user says yes in logs and calls the transfer....what am i doing wrong?
t
Thanks — I see the
finalize_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)
Copy code
@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).
d
JUST TELL ME HOW TO COLLEC THE RESPONSE CORRECTLY
t
Understood — here’s the exact fix to collect the response reliably. Problem: the tool returns mixed types (""/str vs None). The runtime converts tool returns to strings and sends them back to the LLM; returning None can cause the "no" outcome to be ignored. Fix: always return a string status, and ensure you parse the boolean argument passed into the tool. Use explicit logging before returning. Example:
Copy code
@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.
d
async def on_enter(self) -> None: # _if self.userdata.has_existing_booking:_ # _logger.info("_🔮 Bookings Exists") return await self.finalize_call()
t
You're calling
finalize_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).
Copy code
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:
Copy code
@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.
Thanks — I see the issue: when
end_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.
Copy code
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.
d
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 still reads it out loud
t
Thanks — I know this is urgent. You’ve shown
end_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:
Copy code
# 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:
Copy code
@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.
d
end_call is not @functon_tool its only finalaize_call, oyu have the code
t
You're almost there — here's the exact, minimal pattern that collects the yes/no correctly and prevents the function name or return value from being spoken. 1) Ask once (on_enter)
Copy code
async 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
Copy code
@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.
d
why cant i just use generate_reply??? the static say is not ideal
t
You're right — you can use
generate_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)
Copy code
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:
Copy code
@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.