LLMConfigureOutputFrame: allow configuring LLM output

This commit is contained in:
Aleix Conchillo Flaqué
2025-08-26 14:01:48 -07:00
parent 5803936838
commit 16f57be72c
4 changed files with 39 additions and 1 deletions

View File

@@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added support for switching between audio+text to text-only modes within the
same pipeline. This is done by pushing
`LLMConfigureOutputFrame(skip_tts=True)` to enter text-only mode, and
disabling it to return to audio+text. The LLM will still generate tokens and
add them to the context, but they will not be sent to TTS.
- Added `skip_tts` field to `TextFrame`. This lets a text frame bypass TTS while
still being included in the LLM context. Useful for cases like structured text
that isnt meant to be spoken but should still contribute to context.

View File

@@ -607,6 +607,21 @@ class LLMEnablePromptCachingFrame(DataFrame):
enable: bool
@dataclass
class LLMConfigureOutputFrame(DataFrame):
"""Frame to configure LLM output.
This frame is used to configure how the LLM produces output. For example, it
can tell the LLM to generate tokens that should be added to the context but
not spoken by the TTS service (if one is present in the pipeline).
Parameters:
skip_tts: Whether LLM tokens should skip the TTS service (if any).
"""
skip_tts: bool
@dataclass
class TTSSpeakFrame(DataFrame):
"""Frame containing text that should be spoken by TTS.

View File

@@ -103,7 +103,7 @@ class DTMFAggregator(FrameProcessor):
digit_value = frame.button.value
self._aggregation += digit_value
# For first digit, schedule interruption in separate task
# For first digit, schedule interruption.
if is_first_digit:
await self.push_frame(BotInterruptionFrame(), FrameDirection.UPSTREAM)

View File

@@ -37,6 +37,8 @@ from pipecat.frames.frames import (
FunctionCallResultFrame,
FunctionCallResultProperties,
FunctionCallsStartedFrame,
LLMConfigureOutputFrame,
LLMTextFrame,
StartFrame,
StartInterruptionFrame,
UserImageRequestFrame,
@@ -179,6 +181,7 @@ class LLMService(AIService):
self._function_call_tasks: Dict[asyncio.Task, FunctionCallRunnerItem] = {}
self._sequential_runner_task: Optional[asyncio.Task] = None
self._tracing_enabled: bool = False
self._skip_tts: bool = False
self._register_event_handler("on_function_calls_started")
self._register_event_handler("on_completion_timeout")
@@ -272,6 +275,20 @@ class LLMService(AIService):
if isinstance(frame, StartInterruptionFrame):
await self._handle_interruptions(frame)
elif isinstance(frame, LLMConfigureOutputFrame):
self._skip_tts = frame.skip_tts
async def push_frame(self, frame: Frame, direction: FrameDirection = FrameDirection.DOWNSTREAM):
"""Pushes a frame.
Args:
frame: The frame to push.
direction: The direction of frame pushing.
"""
if isinstance(frame, LLMTextFrame):
frame.skip_tts = self._skip_tts
await super().push_frame(frame, direction)
async def _handle_interruptions(self, _: StartInterruptionFrame):
for function_name, entry in self._functions.items():