Move aggregation logic when skip_tts is on to the assistant aggregator

This commit is contained in:
mattie ruth backman
2025-10-30 14:10:56 -04:00
parent bc6a9cac26
commit 29417ba44d
3 changed files with 49 additions and 39 deletions

View File

@@ -22,7 +22,6 @@ from pipecat.audio.interruptions.base_interruption_strategy import BaseInterrupt
from pipecat.audio.turn.smart_turn.base_smart_turn import SmartTurnParams from pipecat.audio.turn.smart_turn.base_smart_turn import SmartTurnParams
from pipecat.audio.vad.vad_analyzer import VADParams from pipecat.audio.vad.vad_analyzer import VADParams
from pipecat.frames.frames import ( from pipecat.frames.frames import (
AggregatedLLMTextFrame,
BotStartedSpeakingFrame, BotStartedSpeakingFrame,
BotStoppedSpeakingFrame, BotStoppedSpeakingFrame,
CancelFrame, CancelFrame,

View File

@@ -24,6 +24,7 @@ from pipecat.audio.interruptions.base_interruption_strategy import BaseInterrupt
from pipecat.audio.turn.smart_turn.base_smart_turn import SmartTurnParams from pipecat.audio.turn.smart_turn.base_smart_turn import SmartTurnParams
from pipecat.audio.vad.vad_analyzer import VADParams from pipecat.audio.vad.vad_analyzer import VADParams
from pipecat.frames.frames import ( from pipecat.frames.frames import (
AggregatedLLMTextFrame,
BotStartedSpeakingFrame, BotStartedSpeakingFrame,
BotStoppedSpeakingFrame, BotStoppedSpeakingFrame,
CancelFrame, CancelFrame,
@@ -47,6 +48,7 @@ from pipecat.frames.frames import (
LLMRunFrame, LLMRunFrame,
LLMSetToolChoiceFrame, LLMSetToolChoiceFrame,
LLMSetToolsFrame, LLMSetToolsFrame,
LLMTextFrame,
SpeechControlParamsFrame, SpeechControlParamsFrame,
StartFrame, StartFrame,
TextFrame, TextFrame,
@@ -66,7 +68,7 @@ from pipecat.processors.aggregators.llm_response import (
LLMUserAggregatorParams, LLMUserAggregatorParams,
) )
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
from pipecat.utils.string import concatenate_aggregated_text from pipecat.utils.string import concatenate_aggregated_text, match_endofsentence
from pipecat.utils.time import time_now_iso8601 from pipecat.utils.time import time_now_iso8601
@@ -595,6 +597,9 @@ class LLMAssistantAggregator(LLMContextAggregator):
self._function_calls_in_progress: Dict[str, Optional[FunctionCallInProgressFrame]] = {} self._function_calls_in_progress: Dict[str, Optional[FunctionCallInProgressFrame]] = {}
self._context_updated_tasks: Set[asyncio.Task] = set() self._context_updated_tasks: Set[asyncio.Task] = set()
self._llm_aggregation: str = ""
self._skip_tts: Optional[bool] = None
@property @property
def has_function_calls_in_progress(self) -> bool: def has_function_calls_in_progress(self) -> bool:
"""Check if there are any function calls currently in progress. """Check if there are any function calls currently in progress.
@@ -618,6 +623,8 @@ class LLMAssistantAggregator(LLMContextAggregator):
await self.push_frame(frame, direction) await self.push_frame(frame, direction)
elif isinstance(frame, LLMFullResponseStartFrame): elif isinstance(frame, LLMFullResponseStartFrame):
await self._handle_llm_start(frame) await self._handle_llm_start(frame)
elif isinstance(frame, LLMTextFrame):
await self._handle_llm_text(frame)
elif isinstance(frame, LLMFullResponseEndFrame): elif isinstance(frame, LLMFullResponseEndFrame):
await self._handle_llm_end(frame) await self._handle_llm_end(frame)
elif isinstance(frame, TextFrame): elif isinstance(frame, TextFrame):
@@ -806,12 +813,50 @@ class LLMAssistantAggregator(LLMContextAggregator):
await self.push_aggregation() await self.push_aggregation()
await self.push_context_frame(FrameDirection.UPSTREAM) await self.push_context_frame(FrameDirection.UPSTREAM)
async def _handle_llm_start(self, _: LLMFullResponseStartFrame): async def _handle_llm_start(self, frame: LLMFullResponseStartFrame):
self._started += 1 self._started += 1
if self._skip_tts is None:
self._skip_tts = frame.skip_tts
await self._maybe_push_llm_aggregation(frame)
async def _handle_llm_end(self, _: LLMFullResponseEndFrame): async def _handle_llm_text(self, frame: LLMTextFrame):
await self._handle_text(frame)
if self._skip_tts or frame.skip_tts:
self._llm_aggregation += frame.text
await self._maybe_push_llm_aggregation(frame)
async def _handle_llm_end(self, frame: LLMFullResponseEndFrame):
self._started -= 1 self._started -= 1
await self.push_aggregation() await self.push_aggregation()
await self._maybe_push_llm_aggregation(frame)
async def _maybe_push_llm_aggregation(
self, frame: LLMFullResponseStartFrame | LLMTextFrame | LLMFullResponseEndFrame
):
should_push = False
if self._skip_tts and not frame.skip_tts:
# if the skip_tts flag switches, to false, push the current aggregation
should_push = True
self._skip_tts = frame.skip_tts
if self._skip_tts:
if self._skip_tts and isinstance(frame, LLMFullResponseEndFrame):
# on end frame, always push the aggregation
should_push = True
elif len(self._llm_aggregation) > 0 and match_endofsentence(self._llm_aggregation):
# push aggregation on end of sentence
should_push = True
if not should_push:
return
text = self._llm_aggregation.lstrip("\n")
if not text.strip():
# don't push empty text
return
llm_frame = AggregatedLLMTextFrame(text=text, aggregated_by="sentence")
await self.push_frame(llm_frame)
self._llm_aggregation = ""
async def _handle_text(self, frame: TextFrame): async def _handle_text(self, frame: TextFrame):
if not self._started or not frame.append_to_context: if not self._started or not frame.append_to_context:

View File

@@ -984,8 +984,6 @@ class RTVIObserver(BaseObserver):
self._last_user_audio_level = 0 self._last_user_audio_level = 0
self._last_bot_audio_level = 0 self._last_bot_audio_level = 0
self._skip_tts = None
if self._params.system_logs_enabled: if self._params.system_logs_enabled:
self._system_logger_id = logger.add(self._logger_sink) self._system_logger_id = logger.add(self._logger_sink)
@@ -1024,16 +1022,6 @@ class RTVIObserver(BaseObserver):
if self._rtvi: if self._rtvi:
await self._rtvi.push_transport_message(model, exclude_none) await self._rtvi.push_transport_message(model, exclude_none)
async def send_aggregated_llm_text(self, text: str, aggregated_by: Optional[str] = None):
"""Send aggregated LLM text as a bot output message.
Args:
text: The aggregated text to send.
aggregated_by: The method of aggregation (e.g., "word", "sentence").
"""
if self._rtvi:
await self._rtvi.push_aggregated_llm_text(text, aggregated_by)
async def on_push_frame(self, data: FramePushed): async def on_push_frame(self, data: FramePushed):
"""Process a frame being pushed through the pipeline. """Process a frame being pushed through the pipeline.
@@ -1171,30 +1159,14 @@ class RTVIObserver(BaseObserver):
message = RTVIBotLLMTextMessage(data=RTVITextMessageData(text=frame.text)) message = RTVIBotLLMTextMessage(data=RTVITextMessageData(text=frame.text))
await self.send_rtvi_message(message) await self.send_rtvi_message(message)
# initialize skip_tts on first LLMTextFrame # TODO: Remove all this logic when we fully deprecate bot-transcription messages.
if self._skip_tts is None:
self._skip_tts = frame.skip_tts
orig_text = self._bot_transcription
self._bot_transcription += frame.text self._bot_transcription += frame.text
if match_endofsentence(self._bot_transcription) and len(self._bot_transcription) > 0: if match_endofsentence(self._bot_transcription) and len(self._bot_transcription) > 0:
# TODO: Remove this message when we fully deprecate bot-transcription messages.
await self.send_rtvi_message( await self.send_rtvi_message(
RTVIBotTranscriptionMessage(data=RTVITextMessageData(text=self._bot_transcription)) RTVIBotTranscriptionMessage(data=RTVITextMessageData(text=self._bot_transcription))
) )
if frame.skip_tts:
await self.send_aggregated_llm_text(
text=self._bot_transcription, aggregated_by="sentence"
)
self._bot_transcription = "" self._bot_transcription = ""
elif not frame.skip_tts and self._skip_tts:
# We just switched from skipping TTS to not skipping TTS.
# Send any dangling transcription.
if len(orig_text) > 0:
await self.send_aggregated_llm_text(text=orig_text, aggregated_by="sentence")
self._bot_transcription = frame.text
self._skip_tts = frame.skip_tts
async def _handle_user_transcriptions(self, frame: Frame): async def _handle_user_transcriptions(self, frame: Frame):
"""Handle user transcription frames.""" """Handle user transcription frames."""
@@ -1428,12 +1400,6 @@ class RTVIProcessor(FrameProcessor):
) )
await self.push_frame(frame) await self.push_frame(frame)
async def push_aggregated_llm_text(self, text: str, aggregated_by: Optional[str] = None):
"""Push an aggregated LLM text frame."""
frame = AggregatedLLMTextFrame(text=text, aggregated_by=aggregated_by)
frame.skip_tts = True
await self.push_frame(frame)
async def handle_message(self, message: RTVIMessage): async def handle_message(self, message: RTVIMessage):
"""Handle an incoming RTVI message. """Handle an incoming RTVI message.