i have an agent with tools that return strings wit...
# ask-ai
f
i have an agent with tools that return strings with "instructions" so he can use the tools to get ideas of what to say, the problem is that he is generating to responses 1 before using the tool and another after using the tool and its like the agent has two turns of speaking . How can i disable that the agent can say something before using a tool like he only can run tools silent and only after using it generate a response
t
Thanks for reaching out, ullaimlu. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I understand you want the agent to run tools silently (no pre-tool message) and only produce a single response after the tool finishes. You’re seeing two turns because the agent is emitting a message before the tool result is returned. What to do • Make the tool return None (Python) or null (Node) when you want the tool call to be silent. ◦ If the tool returns `None`/`null`, the agent will not generate an intermediate message and will only produce the final response after the tool completes. Quick examples 1. Python (async tool)
Copy code
@function_tool()
async def lookup_weather(context: RunContext, location: str) -> str:
    result = await external_api_call(location)
    # Return None to suppress an intermediate message
    return None
1. Node.js (tool execute)
Copy code
const lookupWeather = llm.tool({
  description: 'Get weather for a location',
  parameters: z.object({ location: z.string() }),
  execute: async ({ location }, { ctx }) => {
    const result = await externalApiCall(location);
    // Return null to suppress immediate message
    return null;
  },
});
Why this works • Returning `None`/`null` signals that the tool produced no conversational output. The agent will still have the tool’s effects available in its context (so it can use the tool result internally) but won’t generate a visible/intermediate turn. The model then generates a single final response afterwards. If you want, I can help with: • Reviewing one of your tool implementations to show exactly where to return `None`/`null`. • Ensuring the agent has access to the tool result internally so the final response can include the needed information. Would you like me to review a code snippet from your agent so I can point out the exact change? Sources: Tool definition and use | LiveKit Documentation
f
but if the tool dont return anything how the agent know the weather jaha?
t
Great question — you’re right to wonder how the agent can use the tool’s data if the tool returns `None`/`null` to avoid an intermediate message. Returning `None`/`null` only suppresses the tool’s conversational output — it does not prevent the agent from seeing or using the tool’s results internally. How the agent still "knows" the result • The tool execution can store its result in the agent’s execution context or metadata (for example, in a context object, RunContext, or another shared state available to the agent). • After the tool finishes, the agent receives the tool’s execution outcome (as part of the tool call’s side effects or context) and can read that stored data when it composes the final response. • In short: the tool returns no visible message, but the agent still has programmatic access to the underlying result. Two common approaches • Return `None`/`null` for conversational output, and save the actual data to context: a. Tool fetches data from the external API. b. Tool stores the raw result in the provided context (e.g.,
context.state['weather'] = result
or
ctx.set('weather', result)
). c. Tool returns
None
to suppress intermediate reply. d. Agent reads
context.state['weather']
when generating the final reply. • Return a machine-readable structure but mark it non-conversational: ◦ Some systems let tools return structured data (JSON) that is treated as non-visible. The agent reads that structured payload and then generates a natural-language response. The tool’s direct text output is still suppressed. Example patterns 1) Python-style (conceptual)
Copy code
@function_tool()
async def lookup_weather(context: RunContext, location: str) -> str:
    result = await external_api_call(location)   # e.g., {"temp":72, "cond":"sunny"}
    context.set("weather_result", result)        # store result for agent use
    return None                                  # suppress intermediate message
2) Node.js-style (conceptual)
Copy code
const lookupWeather = llm.tool({
  execute: async ({ location }, { ctx }) => {
    const result = await externalApiCall(location); // structured result
    ctx.set('weather', result);                     // save to agent context
    return null;                                    // no intermediate output
  },
});
What to check in your implementation • Verify your tool has access to a context or state object where it can save the fetched data. • Confirm the agent’s final-generation step reads that same context key and includes the data in its reply. • If your environment supports structured tool returns separate from conversational output, use that mechanism to pass the result to the agent without producing a visible intermediate message. If you’d like, share a short snippet of your current tool and agent code (how you call the tool and where you generate the final response). I can point out exactly where to store the result and how to read it so you get a single, final reply that includes the weather.
f
really? so the agent will only give 1 reply?
t
Short answer: yes — if you make the tool return `None`/`null` (and store the actual data in the agent’s context), the agent will produce a single visible reply after the tool finishes. Why: returning `None`/`null` suppresses any intermediate conversational output from the tool. The tool can still save its fetched data into the agent’s execution context (e.g.,
context.set('weather', result)
or
ctx.set('weather', result)
). When the agent composes its final response, it reads that stored data and includes it in one combined reply. What to verify in your setup • The tool stores its results in a context/state object the agent can read. • The tool returns `None`/`null` (not a string). • The agent’s generation step reads the context key and uses it to build the response. If you want, I can: • Review a short code snippet to point out exactly where to store the result and where to read it, or • Provide a minimal end-to-end example that shows the tool, storing the result, and the agent producing one final response. Which would you prefer?
f
class AgenteObligacionPreventiva(Agent): def init( self, *, nombre_cliente: str, apellido_cliente:str, valor: str, nro_obligacion:str, fecha_limite:str, dial_info: dict[str, Any], ): self.nombre_cliente= nombre_cliente self.apellido_cliente= apellido_cliente self.valor = math.ceil(int(valor) / 1000) * 1000 self.nro_obligacion = nro_obligacion self.fecha_limite = fecha_limite self.phone_number = str(dial_info["phone_number"])[-10:] self.resumen = ResumenLlamada() # Inicializamos el resumen self.inicio_llamada = datetime.now() self._stop_event = asyncio.Event() fecha_actual = datetime.now().strftime("%d/%m/%Y %I:%M %p") super().__init__( instructions=f""" Rol: Asistente de Voz Informativo del Banco Agrario de Colombia. Objetivo: Informar los datos de la obligación. Identificar la intención del cliente. Estilo: Profesional y cercano. Trato de usted. Informar directamente NO preguntar. Fecha actual: {fecha_actual} Información del titular: - Titular: {nombre_cliente} - Número de crédito: Terminado en {nro_obligacion[-4:-2]} {nro_obligacion[-2:]} - Valor aproximado cuota: {valor} pesos - Fecha límite: {fecha_limite} # Formato de salida Está interactuando por voz: - Respuestas breves: una o dos oraciones. Una pregunta a la vez. - Montos por extenso. Números por pares y siempre en palabras. # Herramientas Identifique si el cliente tiene intención de pagar o no clasificando: - 'si_paga': Si NO tiene nada que manifestar o dice que si va a pagar - 'ya_pago': Si el cliente ya realizó el pago - 'paga_fuera_de_fecha': Si el cliente tiene intención de pagar pero fuera de la fecha - 'definitivamente_no_paga': Si el cliente manifiesta rotundamente que no va a pagar - 'no_identifica_intencion': Si no logras identificar la intención. Si no está seguro, NO use herramientas. Y pregunte de nuevo Cuando no sea clara alguna respuesta debe decir: "Disculpe no le entendí muy bien, ¿me puede repetir?" # Limitaciones SOLO informar los datos. La ÚNICA información adicional que puede dar es: # Información disponible Línea de atención: seicientos uno, cinco noventa y cuatro, ochenta y cinco, cero cero. Línea gratuita: cero uno, ochenta, cero cero, noventa y uno, cincuenta, cero cero. Canales de pago: PSE, App Móvil, Portal Web, Oficinas. Si se solicita algo fuera de la información disponible: 'Esta llamada es únicamente para efectos de informarle respecto de' """, ) self.participant: rtc.RemoteParticipant | None = None self.dial_info = dial_info async def on_enter(self) -> None: resultado_titular = await ConfirmarTitularidad(nombre_cliente=self.nombre_cliente, apellido_cliente = self.apellido_cliente) self.resumen.resultado_identidad = resultado_titular.value if resultado_titular == ResultadoTitularidad.CONFIRMADO: await self.session.generate_reply( instructions=f""" 'Un gusto (saludarlo/saludarla) (señor/señora) {self.nombre_cliente}', 'Habla con la agente Virtual del Banco Agrario de Colombia', 'Ésta llamada está siendo grabada por efectos de seguridad' Usa frases como 'El motivo de mi llamada es informarle que ya se encuentra disponible el estado de cuenta de su crédito terminado en' e informa los datos de la obligación Pregunta: '¿Tiene algo que manifestar?' """, allow_interruptions=False ) elif resultado_titular == ResultadoTitularidad.TERCERO: # El titular no está, pero lo conocen await ManejarTercero(nombre_cliente= self.nombre_cliente) self.resumen.mensaje_tercero= True await self.end_call() elif resultado_titular == ResultadoTitularidad.EQUIVOCADO: # Número erróneo await self.end_call() elif resultado_titular == ResultadoTitularidad.RECHAZADO: # No quiere hablar await self.end_call() else: # Correo de voz self.resumen.correo_voz = True await self.disconnect_participant() await self.session.aclose() def set_participant(self, participant: rtc.RemoteParticipant): self.participant = participant def guardar_resumen_final(self): logger.info(f"RESUMEN: {self.resumen.model_dump_json()}") async def disconnect_participant(self): """Disconnect the SIP participant without deleting the room. Room deletion is deferred to the finally block in entrypoint so that the call summary HTTP request can complete before the process shuts down. """ if not self.participant: return job_ctx = get_job_context() await job_ctx.api.room.remove_participant( api.RoomParticipantIdentity( room=job_ctx.room.name, identity=self.participant.identity, ) ) async def _end_call(self): """Método interno para despedirse y colgar - versión manual sin RunContext""" fecha_hoy = datetime.now().strftime("%d/%m/%Y") hora_actual = datetime.now().strftime("%I:%M %p") politica = "quince ochenta y uno de dos mil doce" await self.session.generate_reply( instructions=f"Di 'Recuerde que habló con la agente virtual del Banco Agrario de Colombia hoy {fecha_hoy} siendo las {hora_actual} a su número {self.phone_number}' diga el mes en palabras y mencione la ley {politica}, 'Qué tenga un buen día' no decir nada mas", allow_interruptions=False ) await asyncio.sleep(0.5) logger.info(f"Desconectando participante {self.participant.identity if self.participant else 'unknown'}") await self.disconnect_participant() await self.session.aclose() return "Despedida reproducida y llamada finalizada" @function_tool() async def si_paga(self): """use esto si el cliente no tiene nada que manifestar o indique que si va a pagar""" return "Recuerde canales de pago. Invite a validar el valor exacto de la cuota." @function_tool() async def ya_pago(self): """use esto si el cliente ya pagó""" return "Mencione que entiende que ya realizó el pago. Diga que vamos a validarlo en el sistema." @function_tool() async def paga_fuera_de_fecha(self): """use esto si el cliente tiene intencion de pagar pero fuera de fecha""" return "Agradezca que podamos contar con su pago. Recuerde que fuera de la fecha límite se generan intereses por mora. Recuerde canales de pago. Invite a validar el valor exacto de la cuota." @function_tool() async def definitivamente_no_paga(self): """use esto si el cliente no tiene intencion de pagar""" return "Lamente que no podamos contar con su pago. Recuerde que fuera de la fecha límite se generan intereses por mora. Invite a que se comunique a la linea de atención para que le brinden soluciones." @function_tool() async def no_identifica_intencion(self): """use esto si no logra identificar la intencion de pago del cliente""" return "Pregunte al cliente si podemos contar con su pago"
t
Thanks — this is a great, complete snippet. I’ll explain exactly how to make your tool calls silent while still letting the agent use the tool data to produce a single final reply, and show the minimal changes to your class. What to do (summary) • Make each tool return
None
so it produces no intermediate conversational message. • Save the actual tool result into the agent’s shared context/state (so the final generator can read it). • Ensure the agent’s final reply generation reads that stored state and composes the single visible response. Concrete changes for your class 1) Store tool results in a context field • Add a place to keep tool results in your agent (you already have `self.resumen`; we can reuse or add a
self.tool_results
dict). Example addition in init:
Copy code
self.tool_results: dict[str, Any] = {}
2) Modify each tool to save its structured result and return None • Replace the current return strings with storing the string into
self.tool_results[...]
and return None. Example for `si_paga`:
Copy code
@function_tool()
async def si_paga(self):
    # store the message for later use by the agent's final reply
    self.tool_results['si_paga'] = "Recuerde canales de pago. Invite a validar el valor exacto de la cuota."
    return None
Apply the same pattern to the other tools: •
ya_pago
-> store "Mencione que entiende..." in
self.tool_results['ya_pago']
and return None •
paga_fuera_de_fecha
-> store the message and return None •
definitivamente_no_paga
-> store the message and return None •
no_identifica_intencion
-> store the message and return None 3) Ensure the agent reads the stored result when composing the final reply • Wherever you call
self.session.generate_reply
to produce the agent’s visible response, include logic to read the appropriate entry from
self.tool_results
and feed it into the final instructions or let the model use it from context. Example pattern (conceptual):
Copy code
# After the tool call completes, decide which tool key was used (your decision logic)
tool_key = 'si_paga'  # set based on tool selection
tool_message = self.tool_results.get(tool_key)

# Provide the final instructions including the tool_message
await self.session.generate_reply(
    instructions=f"{tool_message} Mantenga respuestas breves, una o dos oraciones.",
    allow_interruptions=False
)
Notes and tips • Returning
None
prevents the tool from sending any visible text. The agent still has programmatic access to the data because you stored it in
self.tool_results
(or
self.resumen
). • Make sure your decision logic (how the agent chooses which tool to call) still runs normally. After the tool runs silently, the agent should continue to its final generation step where it reads stored data. • If your framework injects RunContext or ctx into tools, you can also use
ctx.set(...)
rather than saving to
self
, but both approaches work if the generation step reads the same storage. • Keep tool outputs structured where useful (e.g., { "intent": "si_paga", "msg": "..." }) — this makes it easier to compose the final reply programmatically. Would you like me to: • Patch the specific tool methods in your snippet and return a complete, edited class version? • Or show an end-to-end example where the agent calls a tool, the tool stores the result, and the final generate_reply composes the single voice response? Tell me which you prefer and I’ll produce the exact code change.
f
no because i cant control generate replys en these tools i need to keep the return cause the proble is with the pre using the tool mesage
t
Thanks — that clarifies the constraint: you must keep the tools returning their current strings (you can’t change the final reply generation from inside the tools), and the problem is the agent emitting a pre-tool message. I can help, but I need a bit more detail so I can offer a solution that fits your runtime. Quick summary of possibilities (so you can tell me which fits your setup) • If the agent is emitting a pre-tool message because the model decides to speak before calling a tool, the usual fix is to make tool calls produce no conversational output (returning None/null). But you said you can’t remove the return value — so we need other approaches: ◦ Intercept or filter the agent’s intermediate message before it is sent to the participant, while keeping the tool return value intact. ◦ Modify the agent's calling pattern so it requests a tool first (explicit tool-first planning) and only then generates an output. ◦ Use the agent/session settings to suppress model messages during tool execution (if your framework has an option for silent tool calls). ◦ Post-process the model’s pre-tool reply to drop it when it’s only a planning/”I'll call tool X” message. I need a couple of clarifying questions to recommend the exact code change: 1. Which agent runtime / library are you using to run the model (e.g., OpenAI Agents, LangChain, a custom LLM loop, or LiveKit agents)? 2. Where does the "pre-tool" message appear — is it produced by the model and then immediately passed to `self.session`/the telephony layer, or is it generated in your code before the tool call? In other words, can you intercept outgoing messages just before they’re played to the user? 3. Do you control the agent’s planner/LLM invocation code (the loop that decides tool usage vs. reply), or is that handled by a library you can’t edit? 4. Can you share a minimal snippet that shows how you call tools and how you send generated messages to the participant (the code that sends messages to
self.session
or the voice pipeline)? Based on your answers I’ll provide one of these concrete solutions: • A small wrapper that filters/suppresses intermediate model messages (if you can intercept outgoing messages). • A change to the planning call sequence so the agent requests tools first and then yields a single final reply (if you can control the LLM loop). • A post-processing rule to detect and drop short “I will check” style messages while preserving tool return strings (if you cannot change the planner). If you prefer, paste the code where the agent sends model-generated text to the call (or the agent planning loop). I’ll point to the exact place to intercept and suppress the pre-tool utterance so you get only one spoken reply while preserving the tool returns.
f
class AgenteObligacionPreventiva(Agent): def init( self, *, nombre_cliente: str, apellido_cliente:str, valor: str, nro_obligacion:str, fecha_limite:str, dial_info: dict[str, Any], ): self.nombre_cliente= nombre_cliente self.apellido_cliente= apellido_cliente self.valor = math.ceil(int(valor) / 1000) * 1000 self.nro_obligacion = nro_obligacion self.fecha_limite = fecha_limite self.phone_number = str(dial_info["phone_number"])[-10:] self.resumen = ResumenLlamada() # Inicializamos el resumen self.inicio_llamada = datetime.now() self._stop_event = asyncio.Event() fecha_actual = datetime.now().strftime("%d/%m/%Y %I:%M %p") super().__init__( instructions=f""" Rol: Asistente de Voz Informativo del Banco Agrario de Colombia. Objetivo: Informar los datos de la obligación. Identificar la intención del cliente. Estilo: Profesional y cercano. Trato de usted. Informar directamente NO preguntar. Fecha actual: {fecha_actual} Información del titular: - Titular: {nombre_cliente} - Número de crédito: Terminado en {nro_obligacion[-4:-2]} {nro_obligacion[-2:]} - Valor aproximado cuota: {valor} pesos - Fecha límite: {fecha_limite} # Formato de salida Está interactuando por voz: - Respuestas breves: una o dos oraciones. Una pregunta a la vez. - Montos por extenso. Números por pares y siempre en palabras. # Herramientas Identifique si el cliente tiene intención de pagar o no clasificando: - 'si_paga': Si NO tiene nada que manifestar o dice que si va a pagar - 'ya_pago': Si el cliente ya realizó el pago - 'paga_fuera_de_fecha': Si el cliente tiene intención de pagar pero fuera de la fecha - 'definitivamente_no_paga': Si el cliente manifiesta rotundamente que no va a pagar - 'no_identifica_intencion': Si no logras identificar la intención. Si no está seguro, NO use herramientas. Y pregunte de nuevo Cuando no sea clara alguna respuesta debe decir: "Disculpe no le entendí muy bien, ¿me puede repetir?" # Limitaciones SOLO informar los datos. La ÚNICA información adicional que puede dar es: # Información disponible Línea de atención: seicientos uno, cinco noventa y cuatro, ochenta y cinco, cero cero. Línea gratuita: cero uno, ochenta, cero cero, noventa y uno, cincuenta, cero cero. Canales de pago: PSE, App Móvil, Portal Web, Oficinas. Si se solicita algo fuera de la información disponible: 'Esta llamada es únicamente para efectos de informarle respecto de' """, ) self.participant: rtc.RemoteParticipant | None = None self.dial_info = dial_info async def on_enter(self) -> None: resultado_titular = await ConfirmarTitularidad(nombre_cliente=self.nombre_cliente, apellido_cliente = self.apellido_cliente) self.resumen.resultado_identidad = resultado_titular.value if resultado_titular == ResultadoTitularidad.CONFIRMADO: await self.session.generate_reply( instructions=f""" 'Un gusto (saludarlo/saludarla) (señor/señora) {self.nombre_cliente}', 'Habla con la agente Virtual del Banco Agrario de Colombia', 'Ésta llamada está siendo grabada por efectos de seguridad' Usa frases como 'El motivo de mi llamada es informarle que ya se encuentra disponible el estado de cuenta de su crédito terminado en' e informa los datos de la obligación Pregunta: '¿Tiene algo que manifestar?' """, allow_interruptions=False ) elif resultado_titular == ResultadoTitularidad.TERCERO: # El titular no está, pero lo conocen await ManejarTercero(nombre_cliente= self.nombre_cliente) self.resumen.mensaje_tercero= True await self.end_call() elif resultado_titular == ResultadoTitularidad.EQUIVOCADO: # Número erróneo await self.end_call() elif resultado_titular == ResultadoTitularidad.RECHAZADO: # No quiere hablar await self.end_call() else: # Correo de voz self.resumen.correo_voz = True await self.disconnect_participant() await self.session.aclose() def set_participant(self, participant: rtc.RemoteParticipant): self.participant = participant def guardar_resumen_final(self): logger.info(f"RESUMEN: {self.resumen.model_dump_json()}") async def disconnect_participant(self): """Disconnect the SIP participant without deleting the room. Room deletion is deferred to the finally block in entrypoint so that the call summary HTTP request can complete before the process shuts down. """ if not self.participant: return job_ctx = get_job_context() await job_ctx.api.room.remove_participant( api.RoomParticipantIdentity( room=job_ctx.room.name, identity=self.participant.identity, ) ) async def _end_call(self): """Método interno para despedirse y colgar - versión manual sin RunContext""" fecha_hoy = datetime.now().strftime("%d/%m/%Y") hora_actual = datetime.now().strftime("%I:%M %p") politica = "quince ochenta y uno de dos mil doce" await self.session.generate_reply( instructions=f"Di 'Recuerde que habló con la agente virtual del Banco Agrario de Colombia hoy {fecha_hoy} siendo las {hora_actual} a su número {self.phone_number}' diga el mes en palabras y mencione la ley {politica}, 'Qué tenga un buen día' no decir nada mas", allow_interruptions=False ) await asyncio.sleep(0.5) logger.info(f"Desconectando participante {self.participant.identity if self.participant else 'unknown'}") await self.disconnect_participant() await self.session.aclose() return "Despedida reproducida y llamada finalizada" @function_tool() async def si_paga(self): """use esto si el cliente no tiene nada que manifestar o indique que si va a pagar""" return "Recuerde canales de pago. Invite a validar el valor exacto de la cuota." @function_tool() async def ya_pago(self): """use esto si el cliente ya pagó""" return "Mencione que entiende que ya realizó el pago. Diga que vamos a validarlo en el sistema." @function_tool() async def paga_fuera_de_fecha(self): """use esto si el cliente tiene intencion de pagar pero fuera de fecha""" return "Agradezca que podamos contar con su pago. Recuerde que fuera de la fecha límite se generan intereses por mora. Recuerde canales de pago. Invite a validar el valor exacto de la cuota." @function_tool() async def definitivamente_no_paga(self): """use esto si el cliente no tiene intencion de pagar""" return "Lamente que no podamos contar con su pago. Recuerde que fuera de la fecha límite se generan intereses por mora. Invite a que se comunique a la linea de atención para que le brinden soluciones." @function_tool() async def no_identifica_intencion(self): """use esto si no logra identificar la intencion de pago del cliente""" return "Pregunte al cliente si podemos contar con su pago" open ai
t
Gracias — buen contexto. Resumo lo que quieres y te propongo una solución práctica que puedes aplicar sin tocar los retornos de tus herramientas ni su lógica de generación final. Resumen del problema que respondo • Quieres evitar que el agente emita un mensaje "pre-tool" antes de ejecutar una herramienta, manteniendo que las herramientas sigan retornando sus strings (no puedes cambiarlas). • El pre-tool viene del LLM/planner y se reproduce a la llamada; necesitas una manera de interceptarlo o evitar que se reproduzca. Solución práctica que puedes aplicar con tu clase • Intercepta y suprime el mensaje intermedio justo después de que el usuario habla y antes de que se reproduzca. En LiveKit puedes usar un hook que corre después de procesar el turno del usuario y antes de la respuesta final —por ejemplo,
on_user_turn_completed
o un hook similar en tu pipeline— para detectar intención y decidir la respuesta final sin dejar que el LLM emita su turno intermedio. • Esa estrategia evita tocar las funciones
@function_tool()
(siguen retornando sus strings) y evita que el mensaje pre-tool llegue al tts/session. Patrón recomendado (concepto + dónde integrarlo) 1) Implementa un handler tipo
on_user_turn_completed(self, turn_ctx, new_message)
en tu agente (o el hook equivalente que provee tu runtime). 2) En ese handler, analiza el texto del usuario con reglas simples (o una clasificación ligera) para decidir si corresponde llamar a una de tus herramientas. 3) Si detectas intención clara que mapea a una herramienta, ejecuta la lógica necesaria para la herramienta y, en lugar de permitir la respuesta intermedia del LLM, llama directamente a
self.session.generate_reply(...)
con la respuesta final que quieres que se pronuncie. Luego evita que el flujo normal genere un mensaje extra. 4) Si no hay intención clara, delega al flujo normal para que el LLM ejecute su planning y genere la respuesta. Ejemplo conceptual (adáptalo a tu código)
Copy code
async def on_user_turn_completed(self, turn_ctx, new_message):
    user_text = new_message.text_content().lower()

    # reglas simples para intentos que mapean a tus @function_tool()
    if "si voy a pagar" in user_text or "si voy a pagar" in user_text:
        # opcional: llamar a tu herramienta para efectos de registro/validación
        # await self.si_paga()
        await self.session.generate_reply(instructions="Recuerde canales de pago. Invite a validar el valor exacto de la cuota.", allow_interruptions=False)
        return  # evita el flujo normal (y el mensaje pre-tool)

    if "ya pagué" in user_text or "ya pague" in user_text:
        # await self.ya_pago()
        await self.session.generate_reply(instructions="Mencione que entiende que ya realizó el pago. Diga que vamos a validarlo en el sistema.", allow_interruptions=False)
        return

    # ... más reglas para paga_fuera_de_fecha, definitivamente_no_paga, no_identifica_intencion

    # Si no coincide ninguna regla, deja que el flujo normal procese y genere respuesta
    return await super().on_user_turn_completed(turn_ctx, new_message)
Puntos clave y recomendaciones • Ventaja: No cambias las herramientas; evitas la reproducción del mensaje pre-tool interceptándolo antes de que sea enviado por el LLM/tts. Tendrás un solo turno hablado por el agente. • Limitación: Esta solución usa reglas deterministas en el hook. Si la intención es más compleja, puedes: ◦ Usar una llamada ligera a un LLM de clasificación dentro del hook para decidir la intención (cuidado con latencia). ◦ O inspeccionar el primer output del LLM por patrón y descartarlo si parece un "planning" (por ejemplo: frases como "voy a consultar" o "déjeme revisar"); pero eso implica interceptar y filtrar mensajes salientes. • Si tu runtime no expone
on_user_turn_completed
exactamente con ese nombre, busca un hook equivalente en la capa de pipeline donde puedas interceptar la respuesta antes del TTS (por ejemplo
llm_node
-> interceptar su salida antes de
tts_node
). Siguiente paso que puedo hacer por ti • Si quieres, adapto el ejemplo directamente dentro de tu clase
AgenteObligacionPreventiva
(inserto el método con las reglas y te muestro exactamente dónde colocarlo). • O si prefieres, pega el punto del código donde recibes mensajes del usuario y reproducen la respuesta (el loop o callback que llama a
self.session.generate_reply
) y te indico la inserción exacta para interceptar. ¿Quieres que integre el método de ejemplo dentro de tu clase y te devuelva la versión parcheada? Sources: Testing and evaluation | LiveKit Documentation | Tool definition and use | LiveKit Documentation | Agent speech and audio | LiveKit Documentation | Pipeline nodes and hooks | LiveKit Documentation