Compare commits

..

2 Commits

Author SHA1 Message Date
James Hush
4150a514d9 docs: add CHANGELOG entry for skip_tts bug fix 2025-11-28 13:57:24 +01:00
James Hush
4f89c99199 fix: preserve skip_tts flag when frames pass through LLM services
Only set skip_tts=True when _skip_tts is configured, rather than
unconditionally overwriting the frame's skip_tts value. This preserves
frames that already have skip_tts=True when passing through LLM services.

Fixes issue where LLMTextFrame with skip_tts=True would exit LLMSwitcher
with skip_tts=False.
2025-11-28 13:54:57 +01:00
9 changed files with 17 additions and 133 deletions

View File

@@ -5,33 +5,10 @@ All notable changes to **Pipecat** will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Changed
- Updated `AICFilter` to use Quail STT as the default model
(`AICModelType.QUAIL_STT`). Quail STT is optimized for human-to-machine
interaction (e.g., voice agents, speech-to-text) and operates at a native
sample rate of 16 kHz with fixed enhancement parameters.
### Deprecated
- The `noise_gate_enable` parameter in `AICFilter` is deprecated and no longer
has any effect. Noise gating is now handled automatically by the AIC VAD
system. Use `AICFilter.create_vad_analyzer()` for VAD functionality instead.
### Fixed
- Fixed an issue in `AWSTranscribeSTTService` where the `region` arg was
always set to `us-east-1` when providing an AWS_REGION env var.
## [0.0.96] - 2025-11-26 🦃 "Happy Thanksgiving!" 🦃
### Added
- Added `AWSBedrockAgentCoreProcessor` to support invoking an AgentCore-hosted
agent in a Pipecat pipeline.
- Enhanced error handling across the framework:
- Added `on_error` callback to `FrameProcessor` for centralized error
@@ -302,8 +279,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Fixed an issue where `LLMTextFrame` with `skip_tts=True` would have its value
overwritten to `False` when passing through LLM services (e.g., `LLMSwitcher`).
The fix preserves frames that already have `skip_tts=True` set.
- Fixed an issue in `AWSBedrockLLMService` where the `aws_region` arg was
always set to `us-east-1` when providing an AWS_REGION env var.
always set to `us-east-1`.
- Fixed an issue with `DeepgramFluxSTTService` where it sometimes failed to reconnect.

View File

@@ -82,13 +82,6 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
llm = AnthropicLLMService(
api_key=os.getenv("ANTHROPIC_API_KEY"),
model="claude-3-7-sonnet-latest",
wait_for_all=True,
params=AnthropicLLMService.InputParams(
max_tokens=16000,
extra={
"thinking": {"type": "enabled", "budget_tokens": 10000},
},
),
)
llm.register_function("get_weather", get_weather)
llm.register_function("get_restaurant_recommendation", fetch_restaurant_recommendation)

View File

@@ -45,7 +45,7 @@ Source = "https://github.com/pipecat-ai/pipecat"
Website = "https://pipecat.ai"
[project.optional-dependencies]
aic = [ "aic-sdk~=1.2.0" ]
aic = [ "aic-sdk~=1.1.0" ]
anthropic = [ "anthropic~=0.49.0" ]
assemblyai = [ "pipecat-ai[websockets-base]" ]
asyncai = [ "pipecat-ai[websockets-base]" ]

View File

@@ -39,7 +39,7 @@ class AICFilter(BaseAudioFilter):
self,
*,
license_key: str = "",
model_type: AICModelType = AICModelType.QUAIL_STT,
model_type: AICModelType = AICModelType.QUAIL_L,
enhancement_level: Optional[float] = 1.0,
voice_gain: Optional[float] = 1.0,
noise_gate_enable: Optional[bool] = True,
@@ -52,27 +52,12 @@ class AICFilter(BaseAudioFilter):
enhancement_level: Optional overall enhancement strength (0.0..1.0).
voice_gain: Optional linear gain applied to detected speech (0.0..4.0).
noise_gate_enable: Optional enable/disable noise gate (default: True).
.. deprecated:: 1.3.0
The `noise_gate_enable` parameter is deprecated and no longer has any effect.
It will be removed in a future version.
"""
self._license_key = license_key
self._model_type = model_type
self._enhancement_level = enhancement_level
self._voice_gain = voice_gain
if noise_gate_enable is not None:
import warnings
with warnings.catch_warnings():
warnings.simplefilter("always")
warnings.warn(
"Parameter `noise_gate_enable` is deprecated and no longer has any effect. "
"It will be removed in a future version. Use AIC VAD instead (create_vad_analyzer()).",
DeprecationWarning,
)
self._noise_gate_enable = noise_gate_enable
self._enabled = True
@@ -164,6 +149,10 @@ class AICFilter(BaseAudioFilter):
)
if self._voice_gain is not None:
self._aic.set_parameter(AICParameter.VOICE_GAIN, float(self._voice_gain))
if self._noise_gate_enable is not None:
self._aic.set_parameter(
AICParameter.NOISE_GATE_ENABLE, 1.0 if bool(self._noise_gate_enable) else 0.0
)
self._aic_ready = True

View File

@@ -563,33 +563,6 @@ class LLMContextFrame(Frame):
context: "LLMContext"
@dataclass
class LLMThinkingTextFrame(DataFrame):
"""Reasoning frame generated by LLM services."""
thinking: str
def __post_init__(self):
super().__post_init__()
# LLM services send text frames with all necessary spaces included
self.includes_inter_frame_spaces = True
def __str__(self):
pts = format_pts(self.pts)
return f"{self.name}(pts: {pts}, thinking: {self.thinking})"
@dataclass
class LLMThinkingSignatureFrame(DataFrame):
"""Reasoning signature frame generated by LLM services."""
signature: str
def __str__(self):
pts = format_pts(self.pts)
return f"{self.name}(pts: {pts}, signature: {self.signature})"
@dataclass
class LLMMessagesFrame(DataFrame):
"""Frame containing LLM messages for chat completion.

View File

@@ -47,8 +47,6 @@ from pipecat.frames.frames import (
LLMRunFrame,
LLMSetToolChoiceFrame,
LLMSetToolsFrame,
LLMThinkingSignatureFrame,
LLMThinkingTextFrame,
SpeechControlParamsFrame,
StartFrame,
TextFrame,
@@ -593,7 +591,6 @@ class LLMAssistantAggregator(LLMContextAggregator):
self._started = 0
self._function_calls_in_progress: Dict[str, Optional[FunctionCallInProgressFrame]] = {}
self._context_updated_tasks: Set[asyncio.Task] = set()
self._thinking: List[TextPartForConcatenation] = []
@property
def has_function_calls_in_progress(self) -> bool:
@@ -604,11 +601,6 @@ class LLMAssistantAggregator(LLMContextAggregator):
"""
return bool(self._function_calls_in_progress)
async def reset(self):
"""Reset the aggregation state."""
await super().reset()
self._thinking = []
async def process_frame(self, frame: Frame, direction: FrameDirection):
"""Process frames for assistant response aggregation and function call management.
@@ -627,10 +619,6 @@ class LLMAssistantAggregator(LLMContextAggregator):
await self._handle_llm_end(frame)
elif isinstance(frame, TextFrame):
await self._handle_text(frame)
elif isinstance(frame, LLMThinkingTextFrame):
await self._handle_thinking(frame)
elif isinstance(frame, LLMThinkingSignatureFrame):
await self._handle_thinking_signature(frame)
elif isinstance(frame, LLMRunFrame):
await self._handle_llm_run(frame)
elif isinstance(frame, LLMMessagesAppendFrame):
@@ -675,14 +663,6 @@ class LLMAssistantAggregator(LLMContextAggregator):
timestamp_frame = LLMContextAssistantTimestampFrame(timestamp=time_now_iso8601())
await self.push_frame(timestamp_frame)
def thinking_string(self) -> str:
"""Get the current thinking as a string.
Returns:
The concatenated thinking string.
"""
return concatenate_aggregated_text(self._thinking)
async def _handle_llm_run(self, frame: LLMRunFrame):
await self.push_context_frame(FrameDirection.UPSTREAM)
@@ -844,35 +824,6 @@ class LLMAssistantAggregator(LLMContextAggregator):
)
)
async def _handle_thinking(self, frame: LLMThinkingTextFrame):
if not self._started:
return
# Make sure we really have text (spaces count, too!)
if len(frame.thinking) == 0:
return
self._thinking.append(
TextPartForConcatenation(
frame.thinking, includes_inter_part_spaces=frame.includes_inter_frame_spaces
)
)
async def _handle_thinking_signature(self, frame: LLMThinkingSignatureFrame):
if not self._started:
return
thinking = self.thinking_string()
self._context.add_message(
{
"role": "assistant",
"content": [
{"type": "thinking", "thinking": thinking, "signature": frame.signature},
],
}
)
def _context_updated_task_finished(self, task: asyncio.Task):
self._context_updated_tasks.discard(task)

View File

@@ -40,8 +40,6 @@ from pipecat.frames.frames import (
LLMFullResponseStartFrame,
LLMMessagesFrame,
LLMTextFrame,
LLMThinkingSignatureFrame,
LLMThinkingTextFrame,
LLMUpdateSettingsFrame,
UserImageRawFrame,
)
@@ -382,10 +380,6 @@ class AnthropicLLMService(LLMService):
completion_tokens_estimate += self._estimate_tokens(
event.delta.partial_json
)
elif hasattr(event.delta, "thinking"):
await self.push_frame(LLMThinkingTextFrame(event.delta.thinking))
elif hasattr(event.delta, "signature"):
await self.push_frame(LLMThinkingSignatureFrame(event.delta.signature))
elif event.type == "content_block_start":
if event.content_block.type == "tool_use":
tool_use_block = event.content_block

View File

@@ -58,7 +58,7 @@ class AWSTranscribeSTTService(STTService):
api_key: Optional[str] = None,
aws_access_key_id: Optional[str] = None,
aws_session_token: Optional[str] = None,
region: Optional[str] = None,
region: Optional[str] = "us-east-1",
sample_rate: int = 16000,
language: Language = Language.EN,
**kwargs,
@@ -69,7 +69,7 @@ class AWSTranscribeSTTService(STTService):
api_key: AWS secret access key. If None, uses AWS_SECRET_ACCESS_KEY environment variable.
aws_access_key_id: AWS access key ID. If None, uses AWS_ACCESS_KEY_ID environment variable.
aws_session_token: AWS session token for temporary credentials. If None, uses AWS_SESSION_TOKEN environment variable.
region: AWS region for the service.
region: AWS region for the service. Defaults to "us-east-1".
sample_rate: Audio sample rate in Hz. Must be 8000 or 16000. Defaults to 16000.
language: Language for transcription. Defaults to English.
**kwargs: Additional arguments passed to parent STTService class.

View File

@@ -292,8 +292,11 @@ class LLMService(AIService):
frame: The frame to push.
direction: The direction of frame pushing.
"""
# Only set skip_tts=True when configured, to preserve frames that already
# have skip_tts=True. This avoids overwriting frames passing through.
if isinstance(frame, (LLMTextFrame, LLMFullResponseStartFrame, LLMFullResponseEndFrame)):
frame.skip_tts = self._skip_tts
if self._skip_tts:
frame.skip_tts = True
await super().push_frame(frame, direction)