Skip to content

workflows: spell the value back once the caller refuses a confirmation - #6990

Open
u9g wants to merge 3 commits into
mainfrom
jason/workflows-readback-escalation
Open

workflows: spell the value back once the caller refuses a confirmation#6990
u9g wants to merge 3 commits into
mainfrom
jason/workflows-readback-escalation

Conversation

@u9g

@u9g u9g commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

GetEmailTask, GetNameTask, GetPhoneNumberTask, GetAddressTask and GetDOBTask read a recorded value back the same way however many times the caller refuses it. A caller whose name sounds like another spelling can never accept that read-back. In a hotel-receptionist simulation (staging run SR_yxUq2cWd3C3D, job SRJ_8GbAvzvRUXAF) the agent had shayne.cole@gmail.com right three separate times and each time said it as a word, which sounds identical to "shane"; the caller re-spelt it six times and the call ended without a booking.

Change

ReadBack (beta/workflows/utils.py) picks the read-back instruction a task hands its model after recording a value: the natural form on the first attempt, the spelled form on every later one. A task records a value again only when the caller did not confirm it, so the second attempt is by definition after a refusal.

Each of the five tasks threads its read-back through it:

task first attempt (unchanged) second and later
email as a word character by character
name as a word letter by letter (verify_spelling=True keeps spelling from the first attempt)
phone in groups digit by digit
address as a whole field by field, street name spelled
date of birth natural spoken date month, day, year one part at a time

The card tasks are deliberately left out: their values are never read back.

Verification

  • tests/test_workflow_readback.py (unit, hermetic): ReadBack picks natural then spelled; each task's second _update_*_impl returns a different instruction from its first and, where the value can be spelled, carries the separated form.
  • Live replay of the failing job's three caller utterances through GetEmailTask on openai/gpt-4.1, audio modality:
    • main: "…shayne.cole@gmail.com. Is this correct?" ×3
    • this branch: word on the first attempt, then "s h a y n e . c o l e @ g m a i l . c o m. Is this correct?" on the second and third.
  • ruff, mypy (livekit.agents.beta.workflows) clean.

Python only: agents-js has no workflow tasks.

u9g added 2 commits August 26, 2026 12:31
…efused

ReadBack picks the read-back instruction a Get*Task hands its model after
recording a value: the natural form the first time, the spelled form on every
later attempt. A task records a value again only when the caller did not
confirm it, and a value that sounds like another cannot be told apart by
hearing it once more.

Not wired into any task yet.
GetEmailTask, GetNameTask, GetPhoneNumberTask, GetAddressTask and GetDOBTask
read a recorded value back the same way however many times the caller
refuses it. A caller whose name sounds like another spelling can never accept
the read-back: the agent had shayne.cole@gmail.com right three times in one
call and each time said it as a word, which is the same sound as shane, so
the caller re-spelt it and the call ended without a booking.

Each task now hands its read-back through ReadBack: natural on the first
attempt, spelled on every later one. The card tasks stay out, their values
are never read back. verify_spelling on GetNameTask keeps spelling from the
first attempt.
@u9g
u9g requested a review from a team as a code owner August 26, 2026 16:43

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

natural="Repeat the address back to the user.",
spelled=(
f"Repeat the address field by field, spelling the street name letter by "
f"letter: {address_fields}"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we provide spaced input so they don't hallucinate letters, like the strawberry quiz?


def instruction(self, *, natural: str, spelled: str) -> str:
self._attempts += 1
return natural if self._attempts == 1 else spelled

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should go one step further like "A as in alpha" for the 3rd time.

Comment on lines +87 to +100
class ReadBack:
"""Escalates a value's confirmation read-back once the first one is not accepted.

A task records the value again only when the caller did not confirm it, so the
second and later read-backs spell the value out instead of saying it naturally:
a value that sounds like another cannot be told apart by hearing it again.
"""

def __init__(self) -> None:
self._attempts = 0

def instruction(self, *, natural: str, spelled: str) -> str:
self._attempts += 1
return natural if self._attempts == 1 else spelled

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This utility seems very tiny? can't we just inline it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, done

A single bool per task replaces the ReadBack helper: once a value has been
recorded, every later read-back of it is spelled. GetNameTask seeds the bool
from verify_spelling, so its separate branch goes away.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants