fix(realtime): complete workflow message transitions

This commit is contained in:
Xin Wang
2026-08-05 11:25:50 +08:00
parent 17b5e10ed1
commit 11374238f3
4 changed files with 115 additions and 7 deletions
@@ -27,6 +27,7 @@ from pipecat.frames.frames import (
OutputTransportMessageUrgentFrame,
StartFrame,
TTSAudioRawFrame,
TTSStoppedFrame,
)
from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.ai_service import AIService
@@ -120,6 +121,7 @@ class QwenAudioRealtimeService(AIService):
self._assistant_text = ""
self._assistant_timestamp = ""
self._greeting_request_item_id: str | None = None
self._entry_trigger_item_id: str | None = None
self._user_transcript_pending = False
self._user_transcript_item_id = ""
self._user_transcript_timestamp = ""
@@ -215,9 +217,27 @@ class QwenAudioRealtimeService(AIService):
await self._cancel_active_response()
await self.broadcast_interruption()
async def request_response(self) -> None:
async def request_response(self, *, trigger_text: str | None = None) -> None:
if trigger_text:
item_id = f"item_{uuid4().hex}"
self._entry_trigger_item_id = item_id
await self._send_event(
{
"type": "conversation.item.create",
"item": {
"id": item_id,
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": trigger_text}],
},
}
)
await self._send_event({"type": "response.create"})
async def wait_for_response_boundary(self) -> None:
"""Wait until Qwen has finished or cancelled the current response."""
await self._response_done.wait()
def set_speech_started_handler(
self,
handler: SpeechStartedHandler | None,
@@ -387,6 +407,7 @@ class QwenAudioRealtimeService(AIService):
self._deferred_assistant_messages.clear()
self._tool_session.clear()
self._function_names.clear()
self._entry_trigger_item_id = None
self._resolve_fixed_speech()
if websocket and websocket.state is State.OPEN:
try:
@@ -412,6 +433,7 @@ class QwenAudioRealtimeService(AIService):
self._session_ready.clear()
self._response_done.set()
self._greeting_request_item_id = None
self._entry_trigger_item_id = None
self._resolve_fixed_speech()
if self._receive_task is asyncio.current_task():
self._receive_task = None
@@ -480,8 +502,10 @@ class QwenAudioRealtimeService(AIService):
status = response.get("status") if isinstance(response, dict) else None
interrupted = status in {"cancelled", "incomplete", "interrupted", "failed"}
self._response_active = False
await self.push_frame(TTSStoppedFrame())
await self._finish_assistant_text(interrupted=interrupted)
await self._delete_greeting_request()
await self._delete_entry_trigger()
self._resolve_fixed_speech()
self._response_done.set()
elif event_type == "response.output_item.added":
@@ -579,6 +603,15 @@ class QwenAudioRealtimeService(AIService):
wait_until_ready=False,
)
async def _delete_entry_trigger(self) -> None:
item_id = self._entry_trigger_item_id
self._entry_trigger_item_id = None
if item_id:
await self._send_event(
{"type": "conversation.item.delete", "item_id": item_id},
wait_until_ready=False,
)
async def _start_user_transcript_turn(
self,
event: dict[str, Any],
@@ -643,6 +676,10 @@ class QwenAudioRealtimeService(AIService):
async def _send_event(
self, payload: dict[str, Any], *, wait_until_ready: bool = True
) -> None:
if payload.get("type") == "response.create":
# Mark the response busy before the server's response.created
# arrives so an immediate confirmation cannot pass the boundary.
self._response_done.clear()
if wait_until_ready and not self._session_ready.is_set():
self._pending_events.append(payload)
return