Repository navigation
Conversation
|
Replay of a captured Deepgram Flux turn (second preflight only adds a question mark) into preemptive-formatting-only.mp4 |
…pt equivalence "1.5" and "15" were considered the same transcript, so a preemptive reply for one could be reused for the other. Only leading and trailing punctuation is now ignored.
A failed preemptive LLM request was kept by an equivalent preflight and then scheduled at end of turn, leaving the user without a reply. A failed attempt is now regenerated.
| for word, _, _ in split_words(text, ignore_punctuation=False, split_character=True): | ||
| start, end = 0, len(word) | ||
| while start < end and is_punctuation(word[start]): | ||
| start += 1 | ||
| while end > start and is_punctuation(word[end - 1]): | ||
| end -= 1 | ||
| if start < end: | ||
| words.append(word[start:end].casefold()) |
There was a problem hiding this comment.
🟡 Adjacent punctuation restarts preemptive replies
When a transcript changes hello world to hello,world, _formatting_free_words treats hello,world as one word. The equivalent reply is discarded and regenerated despite unchanged spoken words.
Learn more
The word splitter divides on whitespace and selected character-based scripts, not on punctuation. Keeping internal punctuation protects meaningful numbers such as 1.5 from matching 15, but it also combines ordinary words when a transcript inserts a comma without a following space. The resulting token lists differ and both preflight deduplication and end-of-turn reuse discard the running reply.
Example: A preflight of hello world yields ["hello", "world"]; a final transcript of hello,world yields ["hello,world"]. The agent sends another LLM request although the spoken words have not changed.
Recommended fix: Split punctuation between alphabetic words into word boundaries when comparing transcripts, while retaining punctuation inside numeric values and other meaning-bearing tokens. Cover adjoining-punctuation and decimal cases in the equivalence tests.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Not changing this. The only cost is one extra LLM request, which is the safe direction: a mismatch regenerates and never reuses a wrong reply. Before this PR, the preflight path regenerated on any change to the string at all. STTs put a space after punctuation between words, so hello,world against hello world does not happen in practice. Splitting on punctuation between letters while keeping it inside numbers adds rules to the comparison and fixes no case we have seen.
What was wrong
AgentActivity.on_preemptive_generationcancels the in-flight preemptive reply and starts a new LLM request on every call.AudioRecognitioncalls it on everyPREFLIGHT_TRANSCRIPT, and on aFINAL_TRANSCRIPTthat is not an exact string match of the last preflight.STTs revise punctuation within a turn. Deepgram Flux through LiveKit Inference (
deepgram/flux-general-en, default settings) often sends a secondEagerEndOfTurnthat only adds a question mark:Across 42 synthesized Flux turns (default settings and
eager_eot_threshold=0.3), 8 had a second preflight that differed only in punctuation; for that question it happened in 7 of 8 takes. Each one threw away the running LLM request and started over, so the head start shrank by the gap between the two preflights (about 120 to 280 ms here) and the LLM was called twice. The restart never changes what the agent says: at end of turn the reply is already reused when the transcripts are_transcripts_equivalent(casefolded, punctuation-insensitive).What changed
on_preemptive_generationreturns early, before cancelling or counting a retry, when the in-flight attempt's transcript is_transcripts_equivalentto the new one. That covers a formatting-only second preflight and a final that only reformats the preflight. A transcript with new words still cancels and restarts, and the end-of-turn check still compares transcript, chat context, tools and tool choice before reusing the reply.Same fix in agents-js: livekit/agents-js#2794
Verification
test_preemptive_generation_restarts_only_on_new_words: a second attempt that only changes punctuation or case keeps the first one (fails on main); one with new words restarts.make type-check,ruff checkandruff format --checkpass.pytest --unit --audio_eotpasses apart from tests that fail the same way on main locally (silero VAD buffer resize, Google credentials) and two loop-monitor timing tests that pass on rerun.AgentSessionagainstopenai/gpt-4.1-minion LiveKit Inference. On main it sends 2 LLM requests and reuses the second withpreemptive_lead_time=0.382s. On this branch it sends 1 and reuses it withpreemptive_lead_time=0.502s.