Skip to content

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

Merged
u9g merged 5 commits into
mainfrom
jason/workflows-readback-escalation
Sep 8, 2026
Merged

workflows: spell the value back once the caller refuses a confirmation#6990
u9g merged 5 commits into
mainfrom
jason/workflows-readback-escalation

Conversation

@u9g

@u9g u9g commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Callers can get stuck correcting a value that sounds identical to another spelling. In hotel-receptionist simulation SR_yxUq2cWd3C3D (job SRJ_8GbAvzvRUXAF), the agent recorded shayne.cole@gmail.com correctly three times but kept reading it as a word; the caller could not distinguish it from “shane” and the call ended without a booking.

Each capture task now switches its confirmation instruction after the first successful update that requests confirmation:

Task First read-back Subsequent read-backs
Email Natural Character by character, with pre-spaced input
Name Natural, or spelled when verify_spelling=True Letter by letter, with pre-spaced input
Phone In groups Digit by digit, with pre-spaced input
Address Whole address Field by field, with the street-address field pre-spaced
Date of birth Natural spoken date Month, day, year one part at a time

The state is an inline boolean in each task. Name and address spelling preserves punctuation and accented characters; address unit, locality, and country remain separate fields. Stored values retain their original formatting. Credit-card tasks do not read their values back and are outside this change.

Escalation depends on the model calling the update tool again. A spoken refusal alone does not guarantee that call: the benchmark found that callers re-spelling a value triggered updates more reliably than simply restating it. This change addresses repeated updates; it does not establish refusal detection.

Validation:

  • tests/test_workflow_readback.py: 10 hermetic cases cover the transition, repeated spelled attempts, spelling from the first attempt, punctuation/accents, and preservation of stored names and address fields.
  • make check passes (formatting, lint, and type checking); make fix leaves all files unchanged.
  • Fresh CI unit tests: 2,634 passed, 5 skipped. Formatting, Python 3.10/3.13 type checking, behavioral evals, provider tests, and review checks also pass. The simulation job is the sole failing check: after authentication was repaired, 9 of 10 scenarios passed, with the remaining simulated-caller refusal described below.
  • The full local run, after allowing test-server sockets and raising the process file limit, reports 2,187 passed, 5 skipped, and 9 room-test failures with ConnectError / invalid HTTP version. Those tests pass in CI; the local full-suite run is not green.
  • The previously posted audio-modality replay through GetEmailTask on openai/gpt-4.1 changed the second and third read-backs from words to separated characters.
  • The posted spelling benchmark supports supplying pre-spaced names: GPT-4.1 name spelling after correction improved from 80% to 95% in its 20-case sample. This revision implements that recommendation; it does not add NATO spelling.

Simulation check investigation:

  • The previous PR run failed in card collection after the simulated caller declined to repeat the full number, and in DOB collection after successful capture followed by a redundant restart.
  • The DOB correction loop also occurred on main before this PR, in “Date of birth corrected while confirming.”
  • The same card scenario failed on main when the simulated caller said “Sorry, I can’t provide credit card details,” contrary to its scenario instructions. The card implementation is unchanged by this PR.
  • CI authentication was repaired on September 8 by updating the URL, API key, and API secret together. Simulation attempt 2 on 8232cb414 registered the worker successfully and ran all 10 scenarios: 9 passed, 1 failed. The name, address, and DOB correction scenarios passed. The remaining failure is “Card number too short on the first try”: the simulated caller declined to provide the test card number for privacy reasons, contrary to its scenario instructions. This matches the existing caller-refusal failure on main noted above.

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.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

✅ 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 thread livekit-agents/livekit/agents/beta/workflows/utils.py Outdated
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.
@u9g

u9g commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

I benched both review questions (should the prompt carry pre-spaced letters so the model doesn't hallucinate them, and should a third attempt escalate to "S as in Sierra"). Bench is tests/bench_readback_spelling.py on jason/readback-spelling-bench, stacked on this branch.

How it works. Part A drives the real GetNameTask / GetAddressTask through AgentSession in audio modality and only rewrites the spelled instruction the tool returns: raw (this PR), spaced (S h a y n e C o l e), nato_raw, nato_spaced. Two scenarios: verify_spelling (spelled on the first turn, nothing spelled in context yet) and respell (caller refuses and spells the name, which is what happened in the failing call). The reply is scored against the known letters. Part B synthesizes a letter-by-letter and a NATO read-back with cartesia/sonic-3, transcribes with deepgram/nova-3, and scores the letters that come back. STT stands in for the caller's ear.

uv run python tests/bench_readback_spelling.py --models openai/gpt-4.1 --dump out.jsonl
uv run python tests/bench_readback_spelling.py --only audio
uv run python tests/bench_readback_spelling.py --rescore out.jsonl   # re-report saved runs

1. Pre-spaced letters: yes, but not because of hallucination

Across ~500 spelled read-backs over five models there was exactly one wrong letter (gpt-4.1-mini dropped the Y in Krzysztof, raw arm). What raw input does cause is the model skipping the spelling and saying the word again, which is the original bug. Every miss below is "did not spell", e.g. gpt-4.1 after the caller had just spelled it: "Just to confirm, your name is spelled Xiomara Okonkwo. Is that correct?"

Exact-spelling rate, names:

model scenario raw spaced
gpt-4.1 (n=20) verify_spelling 100% 100%
gpt-4.1 (n=20) respell 80% 95%
gpt-4.1-mini (n=12) verify_spelling 92% 100%
gpt-4o-mini (n=12) verify_spelling 8% 83%
gpt-4o-mini (n=12) respell 25% 67%
gemini-2.5-flash (n=12) verify_spelling 58% 100%
gemini-2.5-flash (n=12) respell 42% 50%
gemma-4-31b-it (n=12) both 100% 100%

Address (street name, respell, n=12 per cell) is 75–92% in both arms across models; the misses there are also "did not spell". So name.py and address.py should pass the spaced form the way email_address.py and phone_number.py already do.

2. NATO on a later attempt: small audio gain, only viable with pre-built text

TTS→STT round trip, 20 names, one trial each:

read-back exact
letters ("S, H, A, Y, N, E") 85%
NATO ("S as in Sierra, ...") 95%

The three letter misses were single-letter confusions (G→A, M→N, U→E). The one NATO miss was STT turning "T as in Tango" into "niti as in tango". When I regenerated the samples below for this comment every round trip came back exact, so treat the 85/95 split as indicative, not settled.

On the LLM side, the model produced zero wrong code words whenever it did spell, and hit 100% on the first turn given spaced input. After a refusal though, gpt-4.1 ignored the NATO instruction in 17 of 20 nato_raw cases and 4 of 20 nato_spaced cases. If a third stage is added, hand the model the finished "S as in Sierra, H as in Hotel, ..." string rather than asking it to build one.

Audio samples

Files live on an orphan branch of my fork (u9g/agents@e08e241). The mp4 links open GitHub's inline player; the wav links are the bare audio.

Cartesia sonic-3 speaking the read-back the agent would say, followed by what Deepgram nova-3 heard.

Shayne Cole

letters:
▶ shayne_cole_letters.mp4 · wav

That's s h a y n e c o l e. Is this correct?

NATO:
▶ shayne_cole_nato.mp4 · wav

That's s as in Sierra, h as in hotel, a as in alpha, y as in Yankee, n as in November, e as in echo, c as in Charlie, o as in Oscar, l as in Lima, e as in echo. Is this correct?

Dhruv Venkataraghavan (letter miss in the bench run: G heard as A)

letters:
▶ dhruv_venkataraghavan_letters.mp4 · wav

That's d h r u v v e n k a t a r a g h a v a n. Is this correct?

NATO:
▶ dhruv_venkataraghavan_nato.mp4 · wav

Mikaela Thibodeaux (NATO miss in the bench run: "T as in Tango" heard as "niti as in tango")

letters:
▶ mikaela_thibodeaux_letters.mp4 · wav

NATO:
▶ mikaela_thibodeaux_nato.mp4 · wav

That's m as in Mike, I as in India, k as in kilo, a as in alpha, e as in echo, l as in Lima, a as in alpha, t as in tango, h as in hotel, I as in India, b as in bravo, o as in Oscar, d as in delta, e as in echo, a as in alpha, u as in uniform, x as in X-ray. Is this correct?

Tomasz Wojciechowski (letter miss in the bench run: M heard as N)

letters:
▶ tomasz_wojciechowski_letters.mp4 · wav

NATO:
▶ tomasz_wojciechowski_nato.mp4 · wav

Side finding on this PR

On a plain refusal without re-spelling ("No, that's not right. It's Shayne Cole."), gpt-4.1 called update_name again in 3 of 20 cases and update_address in 4 of 12, so the escalation never fires. It reliably fires only when the caller re-spells, which is the failing call's shape, so the incident is still covered, but the description's "a task records a value again only when the caller did not confirm it" overstates when re-recording happens.

Caveats: one trial per cell, n of 12 to 20, and STT is a proxy for a human listener.

u9g added 2 commits September 8, 2026 11:44
Word boundaries in the spelled form produced three consecutive spaces
("S h a y n e   C o l e"). Strip spaces before spelling so every
character is separated by exactly one.

@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 found 1 new potential issue.

Devin Review

)

read_back = (
f"Spell out the name letter by letter for verification: {' '.join(full_name.replace(' ', ''))}"

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.

🟡 Name boundaries vanish during spelling

Names containing spaces inside or between parts lose every boundary through replace. Ann A Bell and Anna Bell produce the same character sequence, so callers cannot identify the stored name. Street-address boundaries disappear through replace too.

Prompt for agents
Preserve audible and textual boundaries between words and name or address components while still pre-spacing characters for reliable letter-by-letter speech. Update GetNameTask._update_name_impl and GetAddressTask._update_address_impl so distinct values such as “Ann A Bell” versus “Anna Bell”, and “North Cliff” versus “Northcliffe”, cannot produce the same spelled read-back. Extend tests/test_workflow_readback.py with ambiguous multi-word pairs and assert that their spelling instructions remain distinguishable without relying on doubled spaces.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@u9g
u9g merged commit c996600 into main Sep 8, 2026
24 of 25 checks passed
@u9g
u9g deleted the jason/workflows-readback-escalation branch September 8, 2026 19:45
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