This message was deleted.
# ask-ai
s
This message was deleted.
t
Thanks for reaching out, Aleh. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I can see why that difference is confusing. You asked why the tool returned the retry message ("I’m sorry, but the information you provided does not match our records...") while the agent later sent "Thank you for verifying your information..." — here’s a concise explanation and steps to debug. What’s happening (short): • The tool’s return value is an internal result from the verification step — it’s one piece of data fed back into the agent/LLM. • The LLM (agent) then decides the final, user-facing message based on the current conversation context and updated state. That means the tool can return a retry or failure string and the agent can still produce a different final message after further processing or a subsequent successful check. Why they can differ: • The tool message may be from an earlier failed attempt; later input succeeded and triggered the final “Thank you…” message. • If the verification sets state (e.g.,
self._verified[self._dob_key] = True
) and
self._check_completion()
becomes true, the agent can switch flow (via
_update_agent(context)
), and the new flow/agent may generate a different final message. • The LLM can rephrase, summarize, or combine tool outputs with other context before sending the final reply. How to debug this in your code (practical steps): 1. Log each tool return and the exact time it was returned. 2. Log state changes immediately after the tool runs: ◦
self._verified
and
self._retry_count
values. 1. Log when
_update_agent(context)
is called and which agent/flow becomes active. 2. Correlate logs: match tool returns to subsequent LLM/agent outputs (timestamps + conversation IDs). 3. Inspect what
self._dob_verified_message
and
self._retry_message
contain to ensure they match expectations. What to check in your specific snippet: • Confirm whether multiple DOB attempts occurred; a retry could produce the tool message first, then a later successful verification produces the “Thank you…” agent message. • Verify that
_update_agent(context)
hands off to another agent that generates the “Thank you” text rather than the verification tool returning that text directly. If you want, share: • A short log excerpt showing the sequence of tool returns, state updates, and final agent messages (with timestamps), or • The values of
self._dob_verified_message
,
self._retry_message
, and the flow that
_update_agent
switches to. I can then point to the exact line where the divergence happens and suggest a minimal code/logging change to make the behavior and outputs match exactly how you expect. Sources: Building multi-agent architectures with LiveKit agents | LiveKit | Pipeline nodes and hooks | LiveKit Documentation
b
i know for sure that flow was as i described i provide wrong dob and expect retry message not "message": "Thank you for verifying your information. How can I assist you further today?"
t
Thanks — I understand you're certain you supplied a wrong DOB and expected the retry message, but instead you saw "Thank you for verifying your information. How can I assist you further today?" Let's walk through why that happens and what to change so the agent prompts a retry when the DOB is incorrect. Acknowledgement • You're asking why a wrong DOB produces a success-looking reply instead of the retry prompt you expect. You're correct that the tool returned the retry message, but the user still saw the "Thank you..." response. Why this happens (concise) • The verification tool's return (retry vs. verified string) is an internal result. The agent/LLM decides the final user message after receiving that result and after any state transitions. • In your flow, completion of the verification task is being treated as "user provided DOB" (i.e., the step finished), regardless of whether the DOB matched the record. Once the task is marked complete, the agent transitions (via
_update_agent(context)
or similar) and the next agent generates the "Thank you..." message. • In short: the flow marks the verification step complete before the correctness check is used to decide whether to prompt again, so the agent continues with a completion response. What to change so a wrong DOB triggers a retry 1. Perform validation before marking the step complete ◦ After the tool compares expected vs provided DOB, only set
self._verified[self._dob_key] = True
and allow completion when the values actually match. ◦ If they do not match, do NOT mark the verification step complete; instead return the retry message and keep the agent in the verification flow. 1. Ensure the agent's completion check depends on correctness, not just user input ◦ Make
_check_completion()
return true only when all required verification flags are true. ◦ Avoid marking the overall task complete simply because the user responded. 1. Stop the handoff to the next agent when validation fails ◦ Call
_update_agent(context)
(or trigger a flow handoff) only after successful validation. ◦ If you currently call
_update_agent(context)
based on "input received" rather than "input validated," change that condition. 1. Add explicit logging and a guard ◦ Log the tool return value,
self._verified[self._dob_key]
, and whether
_update_agent
is called. ◦ Add a guard right before `_update_agent(context)`: ▪︎ If
self._verified[self._dob_key]
is False, skip the update and return the retry prompt. Minimal code checklist (apply to your snippet) • Verify the branch ordering: check validation first, then set verified flag only on match. • Ensure
self._retry_count[self._dob_key] += 1
and
return self._retry_message
run when there is a mismatch, and do NOT call
_update_agent(context)
there. • Ensure the success branch both sets verified flag and only then calls
_update_agent(context)
. If you want, I can: • Suggest a small patched version of your function showing where to gate
_update_agent
. • Review a short log excerpt (tool returns, state changes, timestamps) and point to the exact step where the flow is marked complete prematurely. Which would you prefer? Sources: Building multi-agent architectures with LiveKit agents | LiveKit | Pipeline nodes and hooks | LiveKit Documentation
b
you are wrong here is whole agent
Copy code
from agents.utils.phone_formatter import format_phone_for_speech
from entities.customer import CustomerData, UserLanguage
from entities.session import SessionState
from livekit.agents import ChatContext, RunContext, function_tool
from prompts.provider import (
    collect_info_instructions,
    identity_verify_agent_instructions,
    identity_verify_agent_refused_information_instructions,
    identity_verify_agent_verification_failed_instructions,
)

from .application import ApplicationAgent
from .base import BaseAgent


class IdentityVerifyAgent(BaseAgent):
    def __init__(
        self,
        customer_data: CustomerData,
        company_name: str,
        intro_message: str,
        current_language: UserLanguage = UserLanguage.ENGLISH,
        chat_ctx: ChatContext | None = None,
        user_timezone: str = "UTC",
    ) -> None:
        main_instructions = identity_verify_agent_instructions.invoke(
            {
                "customer_full_name": f"{customer_data.firstname} {customer_data.lastname}",
                "company_name": company_name,
                "intro_message": intro_message,
            }
        ).text
        collect_instructions = collect_info_instructions.invoke({}).text

        instructions = f"{main_instructions}\n\n{collect_instructions}"
        super().__init__(
            instructions=instructions,
            chat_ctx=chat_ctx,
            current_language=current_language,
            user_timezone=user_timezone,
        )

        self._dob_key = "dob"
        self._ssn_key = "ssn"

        self._verified: dict[str, bool] = {self._ssn_key: False, self._dob_key: False}
        self._retry_count: dict[str, int] = {self._ssn_key: 0, self._dob_key: 0}
        self.attempt_limit = 2

        self._retry_message = (
            "I'm sorry, but the information you provided does not match our records. "
            "Would you like to try again? "
        )
        self._dob_verified_message = "Date of birth verified."
        self._ssn_verified_message = "SSN verified."

    async def on_enter(self) -> None:
        await self.session.generate_reply()

    async def _verify_dob(
        self,
        context: RunContext[SessionState],
        month: str | None,
        day: str | None,
        year: str | None,
    ) -> str | ApplicationAgent:
        """Verify customer's date of birth."""
        expected = context.userdata.customer_data
        expected_year, expected_month, expected_day = expected.dob.split("-")

        if expected_year == year and expected_month == month and expected_day == day:
            self._verified[self._dob_key] = True
            if self._check_completion():
                return self._update_agent(context)

            return f"{self._dob_verified_message} Current state: {self._verified}"

        self._retry_count[self._dob_key] += 1
        return self._retry_message

    async def _verify_ssn(
        self,
        context: RunContext[SessionState],
        ssn: str | None,
    ) -> str | ApplicationAgent:
        """Verify customer's SSN."""
        expected = context.userdata.customer_data
        if expected.ssn == ssn:
            self._verified[self._ssn_key] = True
            if self._check_completion():
                return self._update_agent(context)

            return f"{self._ssn_verified_message} Current state: {self._verified}"

        self._retry_count[self._ssn_key] += 1
        return self._retry_message

    def _check_completion(self) -> bool:
        """Check if both DOB and SSN have been verified. Transition to next agent if so."""
        return self._verified[self._dob_key] and self._verified[self._ssn_key]

    def _update_agent(
        self,
        context: RunContext[SessionState],
    ) -> ApplicationAgent:
        context.userdata.call_outcome.identity_verified = True

        return ApplicationAgent(
            customer_data=context.userdata.customer_data,
            company_name=context.userdata.company_name,
            application_details=context.userdata.loan_info,
            chat_ctx=self.chat_ctx,
            current_language=context.userdata.current_language,
            user_timezone=context.userdata.customer_data.timezone,
        )

    def is_exceeded(self) -> bool:
        """Check if either field exceeded max retries and fail if so.

        Returns:
            True if verification failed, False otherwise
        """
        return (
            self._retry_count[self._dob_key] >= self.attempt_limit
            or self._retry_count[self._ssn_key] >= self.attempt_limit
        )

    async def _fail(self, context: RunContext[SessionState]) -> None:
        """Fail with verification failed instructions."""
        context.userdata.call_outcome.identity_verified = False

        raw_phone = context.userdata.servicing_info.servicing_branch_phone_number
        customer_service_number = format_phone_for_speech(
            raw_phone, context.userdata.current_language.value
        )

        instructions = identity_verify_agent_verification_failed_instructions.invoke(
            {
                "customer_service_number": customer_service_number,
            }
        ).text
        await self.session.generate_reply(instructions=instructions)
        await self.hangup()

    @function_tool()
    async def verify_dob(
        self,
        context: RunContext[SessionState],
        month: str | None,
        day: str | None,
        year: str | None,
    ) -> str | ApplicationAgent:
        """Verify customer's date of birth.

        Call this when the customer provides their date of birth.

        Args:
            month: Month of birth as MM or None
            day: Day of birth as DD or None
            year: Year of birth YYYY or None

        Returns:
            Status message with verification result
        """
        dob_result: str | ApplicationAgent = self._dob_verified_message
        if not self._verified[self._dob_key]:
            dob_result = await self._verify_dob(context, month, day, year)

        if self.is_exceeded():
            await self._fail(context)

        return dob_result

    @function_tool()
    async def verify_ssn(
        self,
        context: RunContext[SessionState],
        digit1: str | None,
        digit2: str | None,
        digit3: str | None,
        digit4: str | None,
    ) -> str | ApplicationAgent:
        """Verify customer's SSN last 4 digits.

        Call this with the digits the customer provides.

        Args:
            digit1: First digit of SSN last 4 or None
            digit2: Second digit of SSN last 4 or None
            digit3: Third digit of SSN last 4 or None
            digit4: Fourth digit of SSN last 4 or None

        Returns:
            Status message with verification result
        """
        ssn_result: str | ApplicationAgent = self._ssn_verified_message
        if not self._verified[self._ssn_key]:
            ssn_value = f"{digit1}{digit2}{digit3}{digit4}"
            ssn_result = await self._verify_ssn(context, ssn_value)

        if self.is_exceeded():
            await self._fail(context)

        return ssn_result

    @function_tool()
    async def verify_dob_and_ssn(
        self,
        context: RunContext[SessionState],
        month: str | None,
        day: str | None,
        year: str | None,
        ssn_digit1: str | None,
        ssn_digit2: str | None,
        ssn_digit3: str | None,
        ssn_digit4: str | None,
    ) -> str | ApplicationAgent:
        """Verify both date of birth and SSN in one call.

        Call this when the customer provides both DOB and SSN information together.

        Args:
            month: Month of birth as MM or None
            day: Day of birth as DD or None
            year: Year of birth YYYY or None
            ssn_digit1: First digit of SSN last 4 or None
            ssn_digit2: Second digit of SSN last 4 or None
            ssn_digit3: Third digit of SSN last 4 or None
            ssn_digit4: Fourth digit of SSN last 4 or None

        Returns:
            Status message with verification results for both fields
        """
        dob_result: str | ApplicationAgent = self._dob_verified_message
        if not self._verified[self._dob_key]:
            dob_result = await self._verify_dob(context, month, day, year)

        ssn_result: str | ApplicationAgent = self._ssn_verified_message
        if not self._verified[self._ssn_key]:
            ssn_value = f"{ssn_digit1}{ssn_digit2}{ssn_digit3}{ssn_digit4}"
            ssn_result = await self._verify_ssn(context, ssn_value)

        if self.is_exceeded():
            await self._fail(context)

        if self._check_completion():
            return self._update_agent(context)

        return f"{dob_result} {ssn_result}"

    @function_tool()
    async def refused_information(self, context: RunContext[SessionState]) -> None:
        """Handle when customer refuses to provide verification information.

        Call this when the customer explicitly refuses to provide their DOB or SSN."""
        context.userdata.call_outcome.identity_verified = False

        raw_phone = context.userdata.servicing_info.servicing_branch_phone_number
        customer_service_number = format_phone_for_speech(
            raw_phone, context.userdata.current_language.value
        )

        instructions = identity_verify_agent_refused_information_instructions.invoke(
            {
                "customer_service_number": customer_service_number,
            }
        ).text
        await self.session.generate_reply(instructions=instructions)
        await self.hangup()
there is no issues with updates it is only about why agent ingore tool output and provide wring answer?
t
Thanks — I appreciate the persistence and for pasting the full agent. You’re right to expect the retry message to be the user-facing reply when DOB is wrong. Given your code, here’s exactly why you still see "Thank you for verifying..." and what to change so the agent won’t produce that confirmation after a failed DOB check. Short answer • The tool return (the string your function returns) is not guaranteed to be the final spoken reply. The LiveKit agent runtime treats tool returns as data for the LLM, and the LLM may generate an additional reply after the tool completes. In your code the verification tool returns the retry string, but nothing in the agent prevents the LLM from generating the follow-up confirmation message, so you can end up with both messages (tool string + LLM-generated "Thank you..."). Why that happens in your code • Your verification functions correctly return the retry string when DOB mismatches: ◦ _verify_dob returns self._retry_message and increments retry count. • The public function_tool wrapper verify_dob returns that value back to the LLM. • However, the agent runtime then continues the normal dialogue flow. Unless you force the agent to speak a specific message or prevent the LLM from generating additional text, the LLM may produce a polite closing/confirmation such as "Thank you for verifying your information..." based on overall instructions and conversation context. • In short: your tool returns the retry text, but nothing stops the LLM from generating its own reply afterward — so it appears to “ignore” the tool output. Concrete fixes (pick one) 1) Send the reply yourself from the tool and stop further generation • Replace the returned retry string with an explicit reply and a neutral return value: ◦ Call
await self.session.generate_reply(instructions=self._retry_message)
inside the failure branch. ◦ Return
None
(or a neutral marker) so the LLM has no tool message to echo and you avoid extra replies. • This makes the retry prompt the single, authoritative user-facing message. 2) Force the LLM not to continue speaking on failure (prompt-level control) • Update the agent instructions so the LLM must not generate any extra reply after a failed verification tool call. Example instruction: "If verification fails, only speak the provided retry message and do not generate additional confirmation or closing remarks." • This relies on the LLM honoring instructions, so option (1) is more robust. 3) Short-circuit the flow after a failed verification • After returning the retry, ensure the agent runtime does not proceed to the next step or call
generate_reply
again. • Add an explicit guard in the caller (or after the tool return) to not produce any further reply unless verification succeeded. Minimal code change example (Option 1 — robust) • In _verify_dob, when mismatch: ◦ increment retry count ◦ await self.session.generate_reply(instructions=self._retry_message) ◦ return None • Ensure callers treat
None
as "tool already replied; do not generate again". Extra checks to add • Log tool return,
self._verified
, and whether
_update_agent
was called. • Confirm the LLM isn’t re-prompting itself from conversation instructions (remove any auto-closing phrasing in the agent instructions). If you want, I can: • Draft the small patch (showing exactly where to insert
generate_reply
and the
None
return). • Or show how to change the wrapper logic so callers suppress further generation after a tool reply. Which would you prefer? Sources: Tool definition and use | LiveKit Documentation
b
where import ToolError
t
I don't have the answer you're looking for. You could also try asking your question: • in one of the other Slack channels or • to https://deepwiki.com/livekit/livekit_composite which is trained on all LiveKit source code If you find the answer, please post it here to help others!