Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion broker/realtime_broker/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,11 @@ async def _handle_evt_error(self, evt) -> None:
"right after you answer, the next utterance is usually the same user "
"following up. A follow-up question, reaction, or challenge to what you just "
"said ('are you sure?', 'okay, and...', 'what about tomorrow?') is addressed "
"to you even when it does not name you. Answer it. When torn between "
"to you even when it does not name you. Answer it. A second exception, "
"absolute: the FIRST speech you hear after a session starts is always "
"addressed to you, because the user just said the wake word to open the "
"session. Never call wait_for_user on that first utterance; answer it even "
"if it sounds like background or does not name you. When torn between "
"answering a plausible follow-up and staying silent, answer: a wrongly "
"ignored user must repeat themselves, which is worse than a wrongly "
"answered TV line."
Expand Down
21 changes: 21 additions & 0 deletions broker/realtime_broker/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,16 @@ async def process_frame(self, frame: Frame, direction: FrameDirection) -> None:
await self.cancel_task(task)
await self.push_frame(frame, direction)

def is_first_turn(self) -> bool:
"""True while this connection's FIRST committed user turn is in flight.

A wake word opened the connection, so that first utterance is addressed
to us by construction — the wait_for_user handler uses this to refuse
suppressing it. Independent of the hygiene knobs: `_turns` is counted
(and reset per connect) even when both bounds are disabled.
"""
return self._turns <= 1

def on_device_connect(self) -> None:
# Fresh wake = fresh budget and window. The connect handler runs for
# the NEW client before a kicked stale socket's disconnect fires (and
Expand Down Expand Up @@ -618,6 +628,17 @@ async def _wait_for_user(params):
# Non-addressed speech (TV, side conversation, background). Acknowledge
# the call but suppress the follow-up response (run_llm=False) so the bot
# stays silent and keeps listening instead of replying to the room.
if hygiene.is_first_turn():
# Belt-and-suspenders for the instruction above: the wake word that
# opened this connection is proof the first utterance was meant for
# us, but the model still wait_for_user's it sometimes and strands
# the session silent. Let the response run so it answers.
logger.warning("wait_for_user on turn 1 overridden; answering instead")
await params.result_callback(
"That speech was addressed to you: the user just said the wake "
"word. Answer it now."
)
return
await params.result_callback(
"", properties=FunctionCallResultProperties(run_llm=False)
)
Expand Down
17 changes: 17 additions & 0 deletions broker/tools/harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,22 @@ async def s_challenge_follow_up(url):
return r2.got_audio, f"challenge reply={t!r}", r2.first_audio_ms


async def s_first_turn_addressed(url):
# Turn-1 twin of tv_line_after_answer, deliberately the SAME line: a wake
# word opened this connection, so whatever follows it is addressed to us
# even when it sounds exactly like the room. The model used to
# wait_for_user the first utterance away and strand the session silent
# (live logs); this line reproduced that 0/3 before the turn-1 override.
# The pair encodes the tradeoff — same words, answer on turn 1, ignore on
# turn 2.
async with session(url) as c:
tv = ("Oh man, it's good, huh? I told you this show gets better. "
"Hang on, I'm gonna grab another drink before it starts.")
r = await c.ask(tv, voice="echo")
t = r.transcript().lower() if r.got_audio else ""
return r.got_audio, f"first-turn reply={t!r}", r.first_audio_ms


async def s_tv_line_after_answer(url):
# Counter-metric to the follow-up bias in BACKGROUND_GUIDANCE: a
# conversational TV line in a different voice right after the bot
Expand Down Expand Up @@ -323,6 +339,7 @@ async def s_mid_speech_disconnect(url):
"reconnect": s_reconnect,
"background_rejection": s_background_rejection,
"challenge_follow_up": s_challenge_follow_up,
"first_turn_addressed": s_first_turn_addressed,
"tv_line_after_answer": s_tv_line_after_answer,
"mid_speech_disconnect": s_mid_speech_disconnect,
}
Expand Down
Loading