Make end call tool can be cancelled

This commit is contained in:
Xin Wang
2026-08-12 09:18:40 +08:00
parent fc026e765e
commit d0bb5b3868
15 changed files with 350 additions and 48 deletions

View File

@@ -230,9 +230,14 @@ def message_text(message: dict) -> str:
class UserInputProcessor(FrameProcessor):
"""Validate app inputs and coordinate their interruption boundary."""
def __init__(self, should_ignore_input: Callable[[], bool] | None = None):
def __init__(
self,
should_ignore_input: Callable[[], bool] | None = None,
cancel_pending_end: Callable[[], bool] | None = None,
):
super().__init__()
self._should_ignore_input = should_ignore_input or (lambda: False)
self._cancel_pending_end = cancel_pending_end or (lambda: False)
self._register_event_handler("on_user_input")
self._register_event_handler("on_client_ready")
@@ -256,6 +261,9 @@ class UserInputProcessor(FrameProcessor):
await self.push_frame(frame, direction)
return
if user_input.run_immediately and user_input.interrupt:
self._cancel_pending_end()
if self._should_ignore_input():
logger.debug("当前会话输入已暂停,忽略用户输入")
await self._emit_result(
@@ -371,15 +379,25 @@ class SessionUpdateProcessor(FrameProcessor):
class CallEndingUserMuteStrategy(BaseUserMuteStrategy):
"""Keep user media muted after an end-call tool starts terminating a call."""
"""Mute committed endings and cancel interruptible ones on user activity."""
def __init__(self, is_call_ending: Callable[[], bool]):
def __init__(
self,
should_mute: Callable[[], bool],
*,
is_user_interruption: Callable[[object], bool] | None = None,
cancel_pending_end: Callable[[], bool] | None = None,
):
super().__init__()
self._is_call_ending = is_call_ending
self._should_mute = should_mute
self._is_user_interruption = is_user_interruption or (lambda _frame: False)
self._cancel_pending_end = cancel_pending_end or (lambda: False)
async def process_frame(self, frame) -> bool:
await super().process_frame(frame)
return self._is_call_ending()
if self._is_user_interruption(frame):
self._cancel_pending_end()
return self._should_mute()
@dataclass
@@ -570,9 +588,14 @@ class RealtimeInputAudioGateProcessor(FrameProcessor):
class RealtimeUserInputProcessor(FrameProcessor):
"""Route text-only user-input messages to a realtime service."""
def __init__(self, should_ignore_input: Callable[[], bool] | None = None):
def __init__(
self,
should_ignore_input: Callable[[], bool] | None = None,
cancel_pending_end: Callable[[], bool] | None = None,
):
super().__init__()
self._should_ignore_input = should_ignore_input or (lambda: False)
self._cancel_pending_end = cancel_pending_end or (lambda: False)
self._register_event_handler("on_user_input")
async def process_frame(self, frame, direction: FrameDirection):
@@ -590,6 +613,8 @@ class RealtimeUserInputProcessor(FrameProcessor):
if user_input is None:
await self.push_frame(frame, direction)
return
if user_input.run_immediately and user_input.interrupt:
self._cancel_pending_end()
if self._should_ignore_input():
await self._emit_error(
user_input.input_id,