Merge pull request #2148 from pipecat-ai/filipi/aws_bedrock

Refactoring AWSBedrockLLMService to work async
This commit is contained in:
Filipi da Silva Fuchter
2025-07-08 12:14:28 -03:00
committed by GitHub
4 changed files with 115 additions and 116 deletions

View File

@@ -25,6 +25,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Refactored `AWSBedrockLLMService` and `AWSPollyTTSService` to work
asynchronously using `aioboto3` instead of the `boto3` library.
- The `UserIdleProcessor` now handles the scenario where function calls take - The `UserIdleProcessor` now handles the scenario where function calls take
longer than the idle timeout duration. This allows you to use the longer than the idle timeout duration. This allows you to use the
`UserIdleProcessor` in conjunction with function calls that take a while to `UserIdleProcessor` in conjunction with function calls that take a while to

View File

@@ -42,7 +42,7 @@ Website = "https://pipecat.ai"
[project.optional-dependencies] [project.optional-dependencies]
anthropic = [ "anthropic~=0.49.0" ] anthropic = [ "anthropic~=0.49.0" ]
assemblyai = [ "websockets~=13.1" ] assemblyai = [ "websockets~=13.1" ]
aws = [ "boto3~=1.37.16", "websockets~=13.1" ] aws = [ "aioboto3~=15.0.0", "websockets~=13.1" ]
aws-nova-sonic = [ "aws_sdk_bedrock_runtime~=0.0.2" ] aws-nova-sonic = [ "aws_sdk_bedrock_runtime~=0.0.2" ]
azure = [ "azure-cognitiveservices-speech~=1.42.0"] azure = [ "azure-cognitiveservices-speech~=1.42.0"]
cartesia = [ "cartesia~=2.0.3", "websockets~=13.1" ] cartesia = [ "cartesia~=2.0.3", "websockets~=13.1" ]

View File

@@ -55,7 +55,7 @@ from pipecat.services.llm_service import LLMService
from pipecat.utils.tracing.service_decorators import traced_llm from pipecat.utils.tracing.service_decorators import traced_llm
try: try:
import boto3 import aioboto3
import httpx import httpx
from botocore.config import Config from botocore.config import Config
except ModuleNotFoundError as e: except ModuleNotFoundError as e:
@@ -749,13 +749,17 @@ class AWSBedrockLLMService(LLMService):
read_timeout=300, # 5 minutes read_timeout=300, # 5 minutes
retries={"max_attempts": 3}, retries={"max_attempts": 3},
) )
session = boto3.Session(
aws_access_key_id=aws_access_key, self._aws_session = aioboto3.Session()
aws_secret_access_key=aws_secret_key,
aws_session_token=aws_session_token, # Store AWS session parameters for creating client in async context
region_name=aws_region, self._aws_params = {
) "aws_access_key_id": aws_access_key,
self._client = session.client(service_name="bedrock-runtime", config=client_config) "aws_secret_access_key": aws_secret_key,
"aws_session_token": aws_session_token,
"region_name": aws_region,
"config": client_config,
}
self.set_model_name(model) self.set_model_name(model)
self._settings = { self._settings = {
@@ -903,8 +907,11 @@ class AWSBedrockLLMService(LLMService):
logger.debug(f"Calling AWS Bedrock model with: {request_params}") logger.debug(f"Calling AWS Bedrock model with: {request_params}")
async with self._aws_session.client(
service_name="bedrock-runtime", **self._aws_params
) as client:
# Call AWS Bedrock with streaming # Call AWS Bedrock with streaming
response = self._client.converse_stream(**request_params) response = await client.converse_stream(**request_params)
await self.stop_ttfb_metrics() await self.stop_ttfb_metrics()
@@ -913,7 +920,8 @@ class AWSBedrockLLMService(LLMService):
json_accumulator = "" json_accumulator = ""
function_calls = [] function_calls = []
for event in response["stream"]:
async for event in response["stream"]:
self.reset_watchdog() self.reset_watchdog()
# Handle text content # Handle text content

View File

@@ -30,7 +30,7 @@ from pipecat.transcriptions.language import Language
from pipecat.utils.tracing.service_decorators import traced_tts from pipecat.utils.tracing.service_decorators import traced_tts
try: try:
import boto3 import aioboto3
from botocore.exceptions import BotoCoreError, ClientError from botocore.exceptions import BotoCoreError, ClientError
except ModuleNotFoundError as e: except ModuleNotFoundError as e:
logger.error(f"Exception: {e}") logger.error(f"Exception: {e}")
@@ -177,13 +177,25 @@ class AWSPollyTTSService(TTSService):
params = params or AWSPollyTTSService.InputParams() params = params or AWSPollyTTSService.InputParams()
self._polly_client = boto3.client( # Get credentials from environment variables if not provided
"polly", self._aws_params = {
aws_access_key_id=aws_access_key_id, "aws_access_key_id": aws_access_key_id or os.getenv("AWS_ACCESS_KEY_ID"),
aws_secret_access_key=api_key, "aws_secret_access_key": api_key or os.getenv("AWS_SECRET_ACCESS_KEY"),
aws_session_token=aws_session_token, "aws_session_token": aws_session_token or os.getenv("AWS_SESSION_TOKEN"),
region_name=region, "region_name": region or os.getenv("AWS_REGION", "us-east-1"),
}
# Validate that we have the required credentials
if (
not self._aws_params["aws_access_key_id"]
or not self._aws_params["aws_secret_access_key"]
):
raise ValueError(
"AWS credentials not found. Please provide them either through constructor parameters "
"or set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables."
) )
self._aws_session = aioboto3.Session()
self._settings = { self._settings = {
"engine": params.engine, "engine": params.engine,
"language": self.language_to_service_language(params.language) "language": self.language_to_service_language(params.language)
@@ -199,24 +211,6 @@ class AWSPollyTTSService(TTSService):
self.set_voice(voice_id) self.set_voice(voice_id)
# Get credentials from environment variables if not provided
self._credentials = {
"aws_access_key_id": aws_access_key_id or os.getenv("AWS_ACCESS_KEY_ID"),
"aws_secret_access_key": api_key or os.getenv("AWS_SECRET_ACCESS_KEY"),
"aws_session_token": aws_session_token or os.getenv("AWS_SESSION_TOKEN"),
"region": region or os.getenv("AWS_REGION", "us-east-1"),
}
# Validate that we have the required credentials
if (
not self._credentials["aws_access_key_id"]
or not self._credentials["aws_secret_access_key"]
):
raise ValueError(
"AWS credentials not found. Please provide them either through constructor parameters "
"or set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables."
)
def can_generate_metrics(self) -> bool: def can_generate_metrics(self) -> bool:
"""Check if this service can generate processing metrics. """Check if this service can generate processing metrics.
@@ -279,14 +273,6 @@ class AWSPollyTTSService(TTSService):
Yields: Yields:
Frame: Audio frames containing the synthesized speech. Frame: Audio frames containing the synthesized speech.
""" """
def read_audio_data(**args):
response = self._polly_client.synthesize_speech(**args)
if "AudioStream" in response:
audio_data = response["AudioStream"].read()
return audio_data
return None
logger.debug(f"{self}: Generating TTS [{text}]") logger.debug(f"{self}: Generating TTS [{text}]")
try: try:
@@ -309,12 +295,15 @@ class AWSPollyTTSService(TTSService):
# Filter out None values # Filter out None values
filtered_params = {k: v for k, v in params.items() if v is not None} filtered_params = {k: v for k, v in params.items() if v is not None}
audio_data = await asyncio.to_thread(read_audio_data, **filtered_params) async with self._aws_session.client("polly", **self._aws_params) as polly:
response = await polly.synthesize_speech(**filtered_params)
if not audio_data: if "AudioStream" in response:
logger.error(f"{self} No audio data returned") # Get the streaming body and read it
yield None stream = response["AudioStream"]
return audio_data = await stream.read()
else:
logger.error(f"{self} No audio stream in response")
audio_data = None
audio_data = await self._resampler.resample(audio_data, 16000, self.sample_rate) audio_data = await self._resampler.resample(audio_data, 16000, self.sample_rate)
@@ -332,7 +321,6 @@ class AWSPollyTTSService(TTSService):
yield frame yield frame
yield TTSStoppedFrame() yield TTSStoppedFrame()
except (BotoCoreError, ClientError) as error: except (BotoCoreError, ClientError) as error:
logger.exception(f"{self} error generating TTS: {error}") logger.exception(f"{self} error generating TTS: {error}")
error_message = f"AWS Polly TTS error: {str(error)}" error_message = f"AWS Polly TTS error: {str(error)}"