Skip to content

Commit da3eb46

Browse files
authored
Merge pull request #277 from awslabs/custom-user-agent
Custom user agent
2 parents fd54082 + ab0ccfa commit da3eb46

11 files changed

Lines changed: 252 additions & 20 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .shared import user_agent
2+
3+
user_agent.inject_user_agent()

python/src/multi_agent_orchestrator/agents/amazon_bedrock_agent.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from multi_agent_orchestrator.agents import Agent, AgentOptions, AgentStreamResponse
1515
from multi_agent_orchestrator.types import ConversationMessage, ParticipantRole
1616
from multi_agent_orchestrator.utils import Logger
17+
from multi_agent_orchestrator.shared import user_agent
1718

1819

1920
@dataclass
@@ -69,7 +70,10 @@ def __init__(self, options: AmazonBedrockAgentOptions):
6970
else:
7071
# Create default client using AWS region from options or environment
7172
self.client = boto3.client('bedrock-agent-runtime',
72-
region_name=options.region or os.environ.get('AWS_REGION'))
73+
region_name=options.region or os.environ.get('AWS_REGION'))
74+
75+
user_agent.register_feature_to_client(self.client, feature="bedrock-agent")
76+
7377

7478
# Configure response handling modes
7579
self.streaming = options.streaming

python/src/multi_agent_orchestrator/agents/bedrock_flows_agent.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
from typing import List, Dict, Any, Optional, Callable
22
from dataclasses import dataclass
33
import os
4-
import json
54
import boto3
65
from multi_agent_orchestrator.utils import (Logger, conversation_to_dict)
76
from multi_agent_orchestrator.agents import (Agent, AgentOptions)
87
from multi_agent_orchestrator.types import (ConversationMessage, ParticipantRole)
8+
from multi_agent_orchestrator.shared import user_agent
99

1010
# BedrockFlowsAgentOptions Dataclass
1111
@dataclass
1212
class BedrockFlowsAgentOptions(AgentOptions):
1313
flowIdentifier: str = None
1414
flowAliasIdentifier: str = None
15+
region: Optional[str] = None
1516
bedrock_agent_client: Optional[Any] = None
1617
enableTrace: Optional[bool] = False
1718
flow_input_encoder: Optional[Callable] = None
@@ -28,13 +29,10 @@ def __init__(self, options: BedrockFlowsAgentOptions):
2829
if options.bedrock_agent_client:
2930
self.bedrock_agent_client = options.bedrock_agent_client
3031
else:
31-
if options.region:
32-
self.bedrock_agent_client = boto3.client(
33-
'bedrock-agent-runtime',
34-
region_name=options.region or os.environ.get('AWS_REGION')
35-
)
36-
else:
37-
self.bedrock_agent_client = boto3.client('bedrock-agent-runtime')
32+
self.client = boto3.client('bedrock-agent-runtime',
33+
region_name=options.region or os.environ.get('AWS_REGION'))
34+
35+
user_agent.register_feature_to_client(self.bedrock_agent_client, feature="bedrock-flows-agent")
3836

3937
self.enableTrace = options.enableTrace
4038
self.flowAliasIdentifier = options.flowAliasIdentifier

python/src/multi_agent_orchestrator/agents/bedrock_llm_agent.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
AgentProviderType)
1313
from multi_agent_orchestrator.utils import conversation_to_dict, Logger, AgentTools, AgentTool
1414
from multi_agent_orchestrator.retrievers import Retriever
15-
15+
from multi_agent_orchestrator.shared import user_agent
1616

1717
@dataclass
1818
class BedrockLLMAgentOptions(AgentOptions):
@@ -36,11 +36,13 @@ def __init__(self, options: BedrockLLMAgentOptions):
3636
if options.region:
3737
self.client = boto3.client(
3838
'bedrock-runtime',
39-
region_name=options.region or os.environ.get('AWS_REGION')
40-
)
39+
region_name=options.region)
4140
else:
4241
self.client = boto3.client('bedrock-runtime')
4342

43+
user_agent.register_feature_to_client(self.client, feature="bedrock-llm-agent")
44+
45+
4446
self.model_id: str = options.model_id or BEDROCK_MODEL_ID_CLAUDE_3_HAIKU
4547
self.streaming: bool = options.streaming
4648
self.inference_config: dict[str, Any]

python/src/multi_agent_orchestrator/agents/lambda_agent.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from multi_agent_orchestrator.agents import Agent, AgentOptions
66
from multi_agent_orchestrator.types import ConversationMessage, ParticipantRole
77
from multi_agent_orchestrator.utils import conversation_to_dict
8+
from multi_agent_orchestrator.shared import user_agent
89

910
@dataclass
1011
class LambdaAgentOptions(AgentOptions):
@@ -25,7 +26,11 @@ class LambdaAgent(Agent):
2526
def __init__(self, options: LambdaAgentOptions):
2627
super().__init__(options)
2728
self.options = options
29+
2830
self.lambda_client = boto3.client('lambda', region_name=self.options.function_region)
31+
32+
user_agent.register_feature_to_client(self.lambda_client, feature="lambda-agent")
33+
2934
if self.options.input_payload_encoder is None:
3035
self.encoder = self.__default_input_payload_encoder
3136
else:

python/src/multi_agent_orchestrator/agents/lex_bot_agent.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
from typing import List, Dict, Optional
1+
import os
2+
from typing import Any, Optional
23
from dataclasses import dataclass
34
import boto3
45
from botocore.exceptions import BotoCoreError, ClientError
56
from multi_agent_orchestrator.agents import Agent, AgentOptions
67
from multi_agent_orchestrator.types import ConversationMessage, ParticipantRole
78
from multi_agent_orchestrator.utils import Logger
8-
import os
9-
from typing import Any
9+
from multi_agent_orchestrator.shared import user_agent
1010

1111
@dataclass
1212
class LexBotAgentOptions(AgentOptions):
@@ -30,6 +30,9 @@ def __init__(self, options: LexBotAgentOptions):
3030
else:
3131
self.lex_client = boto3.client('lexv2-runtime', region_name=self.region)
3232

33+
user_agent.register_feature_to_client(self.lex_client, feature="lex-agent")
34+
35+
3336
self.bot_id = options.bot_id
3437
self.bot_alias_id = options.bot_alias_id
3538
self.locale_id = options.locale_id
@@ -38,8 +41,8 @@ def __init__(self, options: LexBotAgentOptions):
3841
raise ValueError("bot_id, bot_alias_id, and locale_id are required for LexBotAgent")
3942

4043
async def process_request(self, input_text: str, user_id: str, session_id: str,
41-
chat_history: List[ConversationMessage],
42-
additional_params: Optional[Dict[str, str]] = None) -> ConversationMessage:
44+
chat_history: list[ConversationMessage],
45+
additional_params: Optional[dict[str, str]] = None) -> ConversationMessage:
4346
try:
4447
params = {
4548
'botId': self.bot_id,

python/src/multi_agent_orchestrator/classifiers/bedrock_classifier.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from multi_agent_orchestrator.utils import Logger
77
from multi_agent_orchestrator.types import ConversationMessage, ParticipantRole, BEDROCK_MODEL_ID_CLAUDE_3_5_SONNET
88
from multi_agent_orchestrator.classifiers import Classifier, ClassifierResult
9-
9+
from multi_agent_orchestrator.shared import user_agent
1010

1111
class BedrockClassifierOptions:
1212
def __init__(
@@ -29,7 +29,10 @@ def __init__(self, options: BedrockClassifierOptions):
2929
if options.client:
3030
self.client = options.client
3131
else:
32-
self.client = boto3.client('bedrock-runtime', region_name=self.region)
32+
self.client = boto3.client('bedrock-runtime',region_name=self.region)
33+
34+
user_agent.register_feature_to_client(self.client, feature="bedrock-classifier")
35+
3336
self.model_id = options.model_id or BEDROCK_MODEL_ID_CLAUDE_3_5_SONNET
3437
self.system_prompt: str
3538
self.inference_config = {

python/src/multi_agent_orchestrator/shared/__init__.py

Whitespace-only changes.
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import logging
2+
import os
3+
4+
from .version import VERSION
5+
6+
mao_version = VERSION
7+
inject_header = True
8+
9+
try:
10+
import botocore
11+
except ImportError:
12+
# if botocore failed to import, user might be using custom runtime and we can't inject header
13+
inject_header = False
14+
15+
logger = logging.getLogger(__name__)
16+
17+
EXEC_ENV = os.environ.get("AWS_EXECUTION_ENV", "NA")
18+
TARGET_SDK_EVENT = "request-created"
19+
FEATURE_PREFIX = "MAOPY"
20+
DEFAULT_FEATURE = "no-op"
21+
HEADER_NO_OP = f"{FEATURE_PREFIX}/{DEFAULT_FEATURE}/{mao_version} MAOPYEnv/{EXEC_ENV}"
22+
23+
24+
def _initializer_botocore_session(session):
25+
"""
26+
This function is used to add an extra header for the User-Agent in the Botocore session,
27+
as described in the pull request: https://github.com/boto/botocore/pull/2682
28+
29+
Parameters
30+
----------
31+
session : botocore.session.Session
32+
The Botocore session to which the user-agent function will be registered.
33+
34+
Raises
35+
------
36+
Exception
37+
If there is an issue while adding the extra header for the User-Agent.
38+
39+
"""
40+
try:
41+
session.register(TARGET_SDK_EVENT, _create_feature_function(DEFAULT_FEATURE))
42+
except Exception:
43+
logger.debug("Can't add extra header User-Agent")
44+
45+
46+
def _create_feature_function(feature):
47+
"""
48+
Create and return the `add_mao_feature` function.
49+
50+
The `add_mao_feature` function is designed to be registered in boto3's event system.
51+
When registered, it appends the given feature string to the User-Agent header of AWS SDK requests.
52+
53+
Parameters
54+
----------
55+
feature : str
56+
The feature string to be appended to the User-Agent header.
57+
58+
Returns
59+
-------
60+
add_mao_feature : Callable
61+
The `add_mao_feature` function that modifies the User-Agent header.
62+
63+
64+
"""
65+
66+
def add_mao_feature(request, **kwargs):
67+
try:
68+
headers = request.headers
69+
header_user_agent = (
70+
f"{headers['User-Agent']} {FEATURE_PREFIX}/{feature}/{mao_version} MAOEnv/{EXEC_ENV}"
71+
)
72+
73+
# This function is exclusive to client and resources objects created in MAO
74+
# and must remove the no-op header, if present
75+
if HEADER_NO_OP in headers["User-Agent"] and feature != DEFAULT_FEATURE:
76+
# Remove HEADER_NO_OP + space
77+
header_user_agent = header_user_agent.replace(f"{HEADER_NO_OP} ", "")
78+
79+
headers["User-Agent"] = f"{header_user_agent}"
80+
except Exception:
81+
logger.debug("Can't find User-Agent header")
82+
83+
return add_mao_feature
84+
85+
86+
# Add feature user-agent to given sdk boto3.session
87+
def register_feature_to_session(session, feature):
88+
"""
89+
Register the given feature string to the event system of the provided boto3 session
90+
and append the feature to the User-Agent header of the request
91+
92+
Parameters
93+
----------
94+
session : boto3.session.Session
95+
The boto3 session to which the feature will be registered.
96+
feature : str
97+
The feature string to be added to the User-Agent header, e.g., "00000001" (Bedrock) in MAO.
98+
99+
Raises
100+
------
101+
AttributeError
102+
If the provided session does not have an event system.
103+
104+
"""
105+
try:
106+
session.events.register(TARGET_SDK_EVENT, _create_feature_function(feature))
107+
except AttributeError as e:
108+
logger.debug(f"session passed in doesn't have a event system:{e}")
109+
110+
111+
# Add feature user-agent to given sdk botocore.session.Session
112+
def register_feature_to_botocore_session(botocore_session, feature):
113+
"""
114+
Register the given feature string to the event system of the provided botocore session
115+
116+
Please notice this function is for patching botocore session and is different from
117+
previous one which is for patching boto3 session
118+
119+
Parameters
120+
----------
121+
botocore_session : botocore.session.Session
122+
The botocore session to which the feature will be registered.
123+
feature : str
124+
The feature value to be added to the User-Agent header, e.g., "00000001" (Bedrock runtime) in MAO.
125+
126+
Raises
127+
------
128+
AttributeError
129+
If the provided session does not have an event system.
130+
131+
Examples
132+
--------
133+
**register led-bot user-agent to botocore session**
134+
135+
>>> from multi_agent_orchestrator.shared.user_agent import (
136+
>>> register_feature_to_botocore_session
137+
>>> )
138+
>>>
139+
>>> session = botocore.session.Session()
140+
>>> register_feature_to_botocore_session(botocore_session=session, feature="data-masking")
141+
>>> key_provider = StrictAwsKmsMasterKeyProvider(key_ids=self.keys, botocore_session=session)
142+
143+
"""
144+
try:
145+
botocore_session.register(TARGET_SDK_EVENT, _create_feature_function(feature))
146+
except AttributeError as e:
147+
logger.debug(f"botocore session passed in doesn't have a event system:{e}")
148+
149+
150+
# Add feature user-agent to given sdk boto3.client
151+
def register_feature_to_client(client, feature):
152+
"""
153+
Register the given feature string to the event system of the provided boto3 client
154+
and append the feature to the User-Agent header of the request
155+
156+
Parameters
157+
----------
158+
client : boto3.session.Session.client
159+
The boto3 client to which the feature will be registered.
160+
feature : str
161+
The feature value to be added to the User-Agent header, e.g., "00000001" (Bedrock runtime) in MAO.
162+
163+
Raises
164+
------
165+
AttributeError
166+
If the provided client does not have an event system.
167+
168+
"""
169+
try:
170+
client.meta.events.register(TARGET_SDK_EVENT, _create_feature_function(feature))
171+
except AttributeError as e:
172+
logger.debug(f"session passed in doesn't have a event system:{e}")
173+
174+
175+
# Add feature user-agent to given sdk boto3.resource
176+
def register_feature_to_resource(resource, feature):
177+
"""
178+
Register the given feature string to the event system of the provided boto3 resource
179+
and append the feature to the User-Agent header of the request
180+
181+
Parameters
182+
----------
183+
resource : boto3.session.Session.resource
184+
The boto3 resource to which the feature will be registered.
185+
feature : str
186+
The feature value to be added to the User-Agent header, e.g., "00000001" (Bedrock runtime) in MAO.
187+
188+
Raises
189+
------
190+
AttributeError
191+
If the provided resource does not have an event system.
192+
193+
"""
194+
try:
195+
resource.meta.client.meta.events.register(TARGET_SDK_EVENT, _create_feature_function(feature))
196+
except AttributeError as e:
197+
logger.debug(f"resource passed in doesn't have a event system:{e}")
198+
199+
200+
def inject_user_agent():
201+
if inject_header:
202+
# Some older botocore versions doesn't support register_initializer. In those cases, we disable the feature.
203+
if not hasattr(botocore, "register_initializer"):
204+
return
205+
206+
# Customize botocore session to inject Boto3 header
207+
# See: https://github.com/boto/botocore/pull/2682
208+
botocore.register_initializer(_initializer_botocore_session)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""Exposes version constant."""
2+
3+
VERSION = "0.1.10"

0 commit comments

Comments
 (0)