Make end call tool can be cancelled
This commit is contained in:
@@ -64,11 +64,12 @@ def playback_marker_for(
|
||||
|
||||
|
||||
class CallEndCoordinator:
|
||||
"""End immediately or after the currently armed closing speech finishes."""
|
||||
"""Coordinate committed and user-cancellable conversation endings."""
|
||||
|
||||
def __init__(self, queue_end: Callable[[str], Awaitable[None]]):
|
||||
self._queue_end = queue_end
|
||||
self._ending = False
|
||||
self._interruptible = False
|
||||
self._armed = False
|
||||
self._speaking = False
|
||||
self._speech_stopped = asyncio.Event()
|
||||
@@ -83,6 +84,15 @@ class CallEndCoordinator:
|
||||
def ending(self) -> bool:
|
||||
return self._ending
|
||||
|
||||
@property
|
||||
def accepts_input(self) -> bool:
|
||||
"""Keep input open while an interruptible ending is still pending."""
|
||||
return not self._finished and (not self._ending or self._interruptible)
|
||||
|
||||
@property
|
||||
def interruptible(self) -> bool:
|
||||
return self._ending and self._interruptible and not self._finished
|
||||
|
||||
@property
|
||||
def speaking(self) -> bool:
|
||||
"""Whether transport output is currently producing bot speech."""
|
||||
@@ -92,16 +102,37 @@ class CallEndCoordinator:
|
||||
"""Wait for the transport-owned bot speech boundary."""
|
||||
await self._speech_stopped.wait()
|
||||
|
||||
def begin(self, reason: str) -> None:
|
||||
def begin(self, reason: str, *, interruptible: bool = False) -> None:
|
||||
if self._finished:
|
||||
return
|
||||
self._ending = True
|
||||
self._interruptible = interruptible
|
||||
self._armed = False
|
||||
self._finish_after_tracked_speech = False
|
||||
self._reason = reason or "completed"
|
||||
|
||||
def cancel_pending_end(self) -> bool:
|
||||
"""Cancel one pending ending when its tool explicitly allows it."""
|
||||
if not self.interruptible:
|
||||
return False
|
||||
self._ending = False
|
||||
self._interruptible = False
|
||||
self._armed = False
|
||||
self._finish_after_tracked_speech = False
|
||||
self._response_speech_started = False
|
||||
# Transport speech state remains authoritative: if audio is currently
|
||||
# playing, the interruption path will still emit BotStoppedSpeakingFrame.
|
||||
logger.info("用户打断结束语,已撤销待挂断状态")
|
||||
return True
|
||||
|
||||
def begin_response(self) -> None:
|
||||
"""Start tracking speech produced by one LLM response."""
|
||||
self._response_speech_started = False
|
||||
|
||||
def arm_after_speech(self) -> None:
|
||||
"""Wait for the next observed bot speech to finish."""
|
||||
if not self._ending:
|
||||
return
|
||||
self._armed = True
|
||||
|
||||
def track_speech(self) -> SpeechPlaybackCompletion:
|
||||
@@ -128,12 +159,16 @@ class CallEndCoordinator:
|
||||
|
||||
async def arm_after_tracked_speech(self) -> None:
|
||||
"""Finish after every already queued fixed utterance has played."""
|
||||
if not self._ending:
|
||||
return
|
||||
self._finish_after_tracked_speech = True
|
||||
if not self._tracked_speech_completions:
|
||||
await self.finish()
|
||||
|
||||
async def finish_after_current_speech(self, *, has_text: bool) -> None:
|
||||
"""Finish now if speech is absent/done, otherwise wait for its stop."""
|
||||
if not self._ending:
|
||||
return
|
||||
if not has_text:
|
||||
await self.finish()
|
||||
return
|
||||
@@ -143,9 +178,10 @@ class CallEndCoordinator:
|
||||
self._armed = True
|
||||
|
||||
async def finish(self) -> None:
|
||||
if self._finished:
|
||||
if self._finished or not self._ending:
|
||||
return
|
||||
self._finished = True
|
||||
self._interruptible = False
|
||||
await self._queue_end(self._reason)
|
||||
|
||||
async def observe(self, frame) -> None:
|
||||
|
||||
Reference in New Issue
Block a user