Merge pull request #4416 from pipecat-ai/mb/pr-4333-aws-credentials-review
feat(aws): add shared credential resolver with boto3 chain fallback
This commit is contained in:
@@ -12,7 +12,6 @@ Amazon Bedrock AgentCore Runtime and streams their responses as LLMTextFrames.
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from collections.abc import Callable
|
||||
|
||||
import aioboto3
|
||||
@@ -27,6 +26,7 @@ from pipecat.frames.frames import (
|
||||
)
|
||||
from pipecat.processors.aggregators.llm_context import LLMContext, LLMSpecificMessage
|
||||
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
||||
from pipecat.services.aws.utils import resolve_credentials
|
||||
|
||||
|
||||
def default_context_to_payload_transformer(
|
||||
@@ -122,8 +122,11 @@ class AWSAgentCoreProcessor(FrameProcessor):
|
||||
|
||||
Args:
|
||||
agentArn: The Amazon Web Services Resource Name (ARN) of the agent.
|
||||
aws_access_key: AWS access key ID. If None, uses default credentials.
|
||||
aws_secret_key: AWS secret access key. If None, uses default credentials.
|
||||
aws_access_key: AWS access key ID. If None, falls back to
|
||||
environment variables and the default boto3 credential chain
|
||||
(instance profiles, IRSA, ECS task roles, SSO, etc.).
|
||||
aws_secret_key: AWS secret access key. Same fallback behaviour as
|
||||
``aws_access_key``.
|
||||
aws_session_token: AWS session token for temporary credentials.
|
||||
aws_region: AWS region.
|
||||
context_to_payload_transformer: Optional callable to transform
|
||||
@@ -139,13 +142,13 @@ class AWSAgentCoreProcessor(FrameProcessor):
|
||||
self._agentArn = agentArn
|
||||
self._aws_session = aioboto3.Session()
|
||||
|
||||
# Store AWS session parameters for creating client in async context
|
||||
self._aws_params = {
|
||||
"aws_access_key_id": aws_access_key or os.getenv("AWS_ACCESS_KEY_ID"),
|
||||
"aws_secret_access_key": aws_secret_key or os.getenv("AWS_SECRET_ACCESS_KEY"),
|
||||
"aws_session_token": aws_session_token or os.getenv("AWS_SESSION_TOKEN"),
|
||||
"region_name": aws_region or os.getenv("AWS_REGION", "us-east-1"),
|
||||
}
|
||||
# Resolve credentials using the shared chain (explicit → env → boto3).
|
||||
self._aws_params = resolve_credentials(
|
||||
aws_access_key_id=aws_access_key,
|
||||
aws_secret_access_key=aws_secret_key,
|
||||
aws_session_token=aws_session_token,
|
||||
region=aws_region,
|
||||
).to_boto_kwargs()
|
||||
|
||||
# Set transformers with defaults
|
||||
self._context_to_payload_transformer = (
|
||||
@@ -204,7 +207,8 @@ class AWSAgentCoreProcessor(FrameProcessor):
|
||||
# aioboto3's `client()` is an async context manager but its stubs don't
|
||||
# advertise `__aenter__` / `__aexit__` in a way pyright can see.
|
||||
async with self._aws_session.client( # pyright: ignore[reportGeneralTypeIssues]
|
||||
"bedrock-agentcore", **self._aws_params
|
||||
"bedrock-agentcore",
|
||||
**self._aws_params, # pyright: ignore[reportArgumentType]
|
||||
) as client:
|
||||
# Invoke the AgentCore agent
|
||||
response = await client.invoke_agent_runtime(
|
||||
|
||||
@@ -13,7 +13,6 @@ function calling.
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
@@ -36,6 +35,7 @@ from pipecat.frames.frames import (
|
||||
from pipecat.metrics.metrics import LLMTokenUsage
|
||||
from pipecat.processors.aggregators.llm_context import LLMContext
|
||||
from pipecat.processors.frame_processor import FrameDirection
|
||||
from pipecat.services.aws.utils import resolve_credentials
|
||||
from pipecat.services.llm_service import LLMService
|
||||
from pipecat.services.settings import NOT_GIVEN, LLMSettings, _NotGiven, assert_given
|
||||
from pipecat.utils.tracing.service_decorators import traced_llm
|
||||
@@ -135,8 +135,11 @@ class AWSBedrockLLMService(LLMService[AWSBedrockLLMAdapter]):
|
||||
.. deprecated:: 0.0.105
|
||||
Use ``settings=AWSBedrockLLMService.Settings(model=...)`` instead.
|
||||
|
||||
aws_access_key: AWS access key ID. If None, uses default credentials.
|
||||
aws_secret_key: AWS secret access key. If None, uses default credentials.
|
||||
aws_access_key: AWS access key ID. If None, falls back to
|
||||
environment variables and the default boto3 credential chain
|
||||
(instance profiles, IRSA, ECS task roles, SSO, etc.).
|
||||
aws_secret_key: AWS secret access key. Same fallback behaviour as
|
||||
``aws_access_key``.
|
||||
aws_session_token: AWS session token for temporary credentials.
|
||||
aws_region: AWS region for the Bedrock service.
|
||||
params: Model parameters and configuration.
|
||||
@@ -215,14 +218,14 @@ class AWSBedrockLLMService(LLMService[AWSBedrockLLMAdapter]):
|
||||
|
||||
self._aws_session = aioboto3.Session()
|
||||
|
||||
# Store AWS session parameters for creating client in async context
|
||||
self._aws_params = {
|
||||
"aws_access_key_id": aws_access_key or os.getenv("AWS_ACCESS_KEY_ID"),
|
||||
"aws_secret_access_key": aws_secret_key or os.getenv("AWS_SECRET_ACCESS_KEY"),
|
||||
"aws_session_token": aws_session_token or os.getenv("AWS_SESSION_TOKEN"),
|
||||
"region_name": aws_region or os.getenv("AWS_REGION", "us-east-1"),
|
||||
"config": client_config,
|
||||
}
|
||||
# Resolve credentials using the shared chain (explicit → env → boto3).
|
||||
resolved = resolve_credentials(
|
||||
aws_access_key_id=aws_access_key,
|
||||
aws_secret_access_key=aws_secret_key,
|
||||
aws_session_token=aws_session_token,
|
||||
region=aws_region,
|
||||
)
|
||||
self._aws_params = {**resolved.to_boto_kwargs(), "config": client_config}
|
||||
|
||||
self._retry_timeout_secs = retry_timeout_secs
|
||||
self._retry_on_timeout = retry_on_timeout
|
||||
|
||||
@@ -11,7 +11,6 @@ speech-to-text transcription with support for multiple languages and audio forma
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
from collections.abc import AsyncGenerator
|
||||
@@ -29,7 +28,12 @@ from pipecat.frames.frames import (
|
||||
StartFrame,
|
||||
TranscriptionFrame,
|
||||
)
|
||||
from pipecat.services.aws.utils import build_event_message, decode_event, get_presigned_url
|
||||
from pipecat.services.aws.utils import (
|
||||
build_event_message,
|
||||
decode_event,
|
||||
get_presigned_url,
|
||||
resolve_credentials,
|
||||
)
|
||||
from pipecat.services.settings import STTSettings, assert_given
|
||||
from pipecat.services.stt_latency import AWS_TRANSCRIBE_TTFS_P99
|
||||
from pipecat.services.stt_service import WebsocketSTTService
|
||||
@@ -81,9 +85,12 @@ class AWSTranscribeSTTService(WebsocketSTTService):
|
||||
"""Initialize the AWS Transcribe STT service.
|
||||
|
||||
Args:
|
||||
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.
|
||||
api_key: AWS secret access key. If None, falls back to environment
|
||||
variables and the default boto3 credential chain (instance
|
||||
profiles, IRSA, ECS task roles, SSO, etc.).
|
||||
aws_access_key_id: AWS access key ID. Same fallback behaviour as
|
||||
``api_key``.
|
||||
aws_session_token: AWS session token for temporary credentials.
|
||||
region: AWS region for the service.
|
||||
sample_rate: Audio sample rate in Hz. If None, uses the pipeline sample rate.
|
||||
AWS Transcribe only supports 8000 or 16000 Hz; other values are
|
||||
@@ -129,11 +136,19 @@ class AWSTranscribeSTTService(WebsocketSTTService):
|
||||
self._show_speaker_label = False
|
||||
self._enable_channel_identification = False
|
||||
|
||||
# Resolve credentials using the shared chain (explicit → env → boto3).
|
||||
resolved = resolve_credentials(
|
||||
aws_access_key_id=aws_access_key_id,
|
||||
aws_secret_access_key=api_key,
|
||||
aws_session_token=aws_session_token,
|
||||
region=region,
|
||||
)
|
||||
|
||||
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"),
|
||||
"aws_access_key_id": resolved.access_key,
|
||||
"aws_secret_access_key": resolved.secret_key,
|
||||
"aws_session_token": resolved.session_token,
|
||||
"region": resolved.region,
|
||||
}
|
||||
|
||||
self._receive_task = None
|
||||
|
||||
@@ -10,7 +10,6 @@ This module provides integration with Amazon Polly for text-to-speech synthesis,
|
||||
supporting multiple languages, voices, and SSML features.
|
||||
"""
|
||||
|
||||
import os
|
||||
from collections.abc import AsyncGenerator
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
@@ -23,6 +22,7 @@ from pipecat.frames.frames import (
|
||||
Frame,
|
||||
TTSAudioRawFrame,
|
||||
)
|
||||
from pipecat.services.aws.utils import resolve_credentials
|
||||
from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven
|
||||
from pipecat.services.tts_service import TTSService
|
||||
from pipecat.transcriptions.language import Language, resolve_language
|
||||
@@ -191,8 +191,11 @@ class AWSPollyTTSService(TTSService):
|
||||
"""Initializes the AWS Polly TTS service.
|
||||
|
||||
Args:
|
||||
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.
|
||||
api_key: AWS secret access key. If None, falls back to environment
|
||||
variables and the default boto3 credential chain (instance
|
||||
profiles, IRSA, ECS task roles, SSO, etc.).
|
||||
aws_access_key_id: AWS access key ID. Same fallback behaviour as
|
||||
``api_key``.
|
||||
aws_session_token: AWS session token for temporary credentials.
|
||||
region: AWS region for Polly service. Defaults to 'us-east-1'.
|
||||
voice_id: Voice ID to use for synthesis. Defaults to 'Joanna'.
|
||||
@@ -250,13 +253,13 @@ class AWSPollyTTSService(TTSService):
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# Get credentials from environment variables if not provided
|
||||
self._aws_params = {
|
||||
"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_name": region or os.getenv("AWS_REGION", "us-east-1"),
|
||||
}
|
||||
# Resolve credentials using the shared chain (explicit → env → boto3).
|
||||
self._aws_params = resolve_credentials(
|
||||
aws_access_key_id=aws_access_key_id,
|
||||
aws_secret_access_key=api_key,
|
||||
aws_session_token=aws_session_token,
|
||||
region=region,
|
||||
).to_boto_kwargs()
|
||||
|
||||
self._aws_session = aioboto3.Session()
|
||||
|
||||
@@ -348,7 +351,8 @@ class AWSPollyTTSService(TTSService):
|
||||
# aioboto3's `client()` is an async context manager but its stubs
|
||||
# don't advertise `__aenter__` / `__aexit__` to pyright.
|
||||
async with self._aws_session.client( # pyright: ignore[reportGeneralTypeIssues]
|
||||
"polly", **self._aws_params
|
||||
"polly",
|
||||
**self._aws_params, # pyright: ignore[reportArgumentType]
|
||||
) as polly:
|
||||
response = await polly.synthesize_speech(**filtered_params)
|
||||
if "AudioStream" in response:
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
# SPDX-License-Identifier: BSD 2-Clause License
|
||||
#
|
||||
|
||||
"""AWS Transcribe utility functions and classes for WebSocket streaming.
|
||||
"""AWS utility functions for Pipecat services.
|
||||
|
||||
This module provides utilities for creating presigned URLs, building event messages,
|
||||
and handling AWS event stream protocol for real-time transcription services.
|
||||
This module provides shared credential resolution and AWS Transcribe utilities
|
||||
for creating presigned URLs, building event messages, and handling AWS event
|
||||
stream protocol for real-time transcription services.
|
||||
"""
|
||||
|
||||
import binascii
|
||||
@@ -15,8 +16,98 @@ import datetime
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
import urllib.parse
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from loguru import logger
|
||||
|
||||
|
||||
@dataclass
|
||||
class AWSCredentials:
|
||||
"""Resolved AWS credentials ready for use by any AWS service."""
|
||||
|
||||
access_key: str | None
|
||||
secret_key: str | None
|
||||
session_token: str | None
|
||||
region: str
|
||||
|
||||
def to_boto_kwargs(self) -> dict[str, str | None]:
|
||||
"""Return credentials as kwargs accepted by ``boto3``/``aioboto3`` clients."""
|
||||
return {
|
||||
"aws_access_key_id": self.access_key,
|
||||
"aws_secret_access_key": self.secret_key,
|
||||
"aws_session_token": self.session_token,
|
||||
"region_name": self.region,
|
||||
}
|
||||
|
||||
|
||||
def resolve_credentials(
|
||||
*,
|
||||
aws_access_key_id: str | None = None,
|
||||
aws_secret_access_key: str | None = None,
|
||||
aws_session_token: str | None = None,
|
||||
region: str | None = None,
|
||||
) -> AWSCredentials:
|
||||
"""Resolve AWS credentials using the standard fallback chain.
|
||||
|
||||
Resolution order:
|
||||
1. Explicit parameters
|
||||
2. Environment variables (``AWS_ACCESS_KEY_ID``, ``AWS_SECRET_ACCESS_KEY``,
|
||||
``AWS_SESSION_TOKEN``, ``AWS_REGION``)
|
||||
3. Default boto3/botocore credential chain (instance profiles, IRSA,
|
||||
ECS task roles, SSO, credential files, etc.)
|
||||
|
||||
The boto3 fallback (step 3) is only attempted when *both* access key and
|
||||
secret key are still unresolved after steps 1-2. This avoids replacing
|
||||
explicitly provided credentials with ambient ones.
|
||||
|
||||
Args:
|
||||
aws_access_key_id: Explicit access key ID.
|
||||
aws_secret_access_key: Explicit secret access key.
|
||||
aws_session_token: Explicit session token.
|
||||
region: Explicit AWS region.
|
||||
|
||||
Returns:
|
||||
An :class:`AWSCredentials` instance. ``access_key`` and
|
||||
``secret_key`` may still be ``None`` if no credentials could be
|
||||
resolved (the caller should raise an appropriate error).
|
||||
"""
|
||||
access_key = aws_access_key_id or os.getenv("AWS_ACCESS_KEY_ID")
|
||||
secret_key = aws_secret_access_key or os.getenv("AWS_SECRET_ACCESS_KEY")
|
||||
session_token = aws_session_token or os.getenv("AWS_SESSION_TOKEN")
|
||||
resolved_region = region or os.getenv("AWS_REGION", "us-east-1")
|
||||
|
||||
# Fall back to the boto3 credential provider chain (pod roles, IRSA,
|
||||
# instance profiles, SSO, credential files, etc.) when explicit
|
||||
# credentials were not supplied.
|
||||
if not access_key and not secret_key:
|
||||
try:
|
||||
import boto3
|
||||
|
||||
session = boto3.Session(region_name=resolved_region)
|
||||
creds = session.get_credentials()
|
||||
if creds:
|
||||
frozen = creds.get_frozen_credentials()
|
||||
access_key = access_key or frozen.access_key
|
||||
secret_key = secret_key or frozen.secret_key
|
||||
session_token = session_token or frozen.token
|
||||
except ImportError:
|
||||
logger.debug(
|
||||
"boto3 not available for credential chain fallback; "
|
||||
"install pipecat-ai[aws] for full credential support."
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to resolve AWS credentials via boto3 chain: {e}")
|
||||
|
||||
return AWSCredentials(
|
||||
access_key=access_key,
|
||||
secret_key=secret_key,
|
||||
session_token=session_token,
|
||||
region=resolved_region,
|
||||
)
|
||||
|
||||
|
||||
def get_presigned_url(
|
||||
|
||||
Reference in New Issue
Block a user