Skip to content

Commit 4c4c419

Browse files
stanek-michalMichal Stanek
authored and
Michal Stanek
committed
Address review comments
1 parent 01521d5 commit 4c4c419

File tree

8 files changed

+155
-79
lines changed

8 files changed

+155
-79
lines changed

instrumentation/opentelemetry-instrumentation-botocore/examples/bedrock-runtime/zero-code/invoke_agent.py

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,30 @@
11
#!/usr/bin/env python3
22

3-
import boto3
43
import json
5-
import uuid
64
import logging
5+
import os
6+
import uuid
7+
8+
import boto3
79
from botocore.exceptions import ClientError
810

911
# Configure logging
1012
logging.basicConfig(level=logging.INFO)
1113
logger = logging.getLogger(__name__)
1214

15+
1316
class WeatherAgentDemo:
1417
def __init__(self):
1518
# Initialize Bedrock Agents Runtime client
16-
self.client = boto3.client('bedrock-agent-runtime')
19+
self.client = boto3.client("bedrock-agent-runtime")
1720

18-
# Replace these with your actual agent IDs
19-
self.agent_id = "TFMZVIWXR7"
20-
self.agent_alias_id = "ZT7YYHO8TO"
21+
self.agent_id = os.environ.get("BEDROCK_AGENT_ID")
22+
self.agent_alias_id = os.environ.get("BEDROCK_AGENT_ALIAS_ID")
23+
24+
if not self.agent_id or not self.agent_alias_id:
25+
raise ValueError(
26+
"Environment variables BEDROCK_AGENT_ID and BEDROCK_AGENT_ALIAS_ID must be set."
27+
)
2128

2229
# Generate a unique session ID for this conversation
2330
self.session_id = str(uuid.uuid4())
@@ -54,12 +61,15 @@ def invoke_agent(self, input_text, session_state=None, end_session=False):
5461
trace_data.append(event["trace"])
5562
elif "returnControl" in event:
5663
# Handle the case where the agent returns control
57-
return {"type": "return_control", "data": event["returnControl"]}
64+
return {
65+
"type": "return_control",
66+
"data": event["returnControl"],
67+
}
5868

5969
return {
6070
"type": "completion",
6171
"completion": completion,
62-
"trace": trace_data
72+
"trace": trace_data,
6373
}
6474

6575
except ClientError as e:
@@ -73,21 +83,23 @@ def handle_return_control(self, return_control_data):
7383

7484
# Simulate weather API response (using function result format)
7585
weather_response = {
76-
"actionGroup": invocation_inputs["functionInvocationInput"]["actionGroup"],
77-
"function": invocation_inputs["functionInvocationInput"]["function"],
86+
"actionGroup": invocation_inputs["functionInvocationInput"][
87+
"actionGroup"
88+
],
89+
"function": invocation_inputs["functionInvocationInput"][
90+
"function"
91+
],
7892
"responseBody": {
79-
"TEXT": {
80-
"body": "It's 55F in Seattle today, rainy."
81-
}
82-
}
93+
"TEXT": {"body": "It's 55F in Seattle today, rainy."}
94+
},
8395
}
8496

8597
# Prepare session state with the returned control results
8698
session_state = {
8799
"invocationId": invocation_id,
88100
"returnControlInvocationResults": [
89101
{"functionResult": weather_response}
90-
]
102+
],
91103
}
92104

93105
return session_state
@@ -110,7 +122,7 @@ def run_demo(self):
110122
final_response = self.invoke_agent(
111123
"Process the weather data",
112124
session_state=session_state,
113-
end_session=False # Keeping session open for potential follow-up
125+
end_session=False, # Keeping session open for potential follow-up
114126
)
115127

116128
if final_response["type"] == "completion":
@@ -122,11 +134,13 @@ def run_demo(self):
122134
print(json.dumps(trace, indent=2))
123135
else:
124136
# Should not happen if session_state is provided correctly
125-
logger.warning(f"Unexpected response type after providing session state: {final_response.get('type')}")
137+
logger.warning(
138+
f"Unexpected response type after providing session state: {final_response.get('type')}"
139+
)
126140
print("Received unexpected response after providing data.")
127141

128142
elif response["type"] == "completion":
129-
# Agent answered directly without needing function call/return control
143+
# Agent answered directly without needing function call/return control
130144
print("\nAgent responded directly:")
131145
print(response["completion"])
132146
if response["trace"]:
@@ -135,12 +149,17 @@ def run_demo(self):
135149
print(json.dumps(trace, indent=2))
136150
else:
137151
# Handle other unexpected response types
138-
logger.error(f"Unexpected initial response type: {response.get('type')}")
152+
logger.error(
153+
f"Unexpected initial response type: {response.get('type')}"
154+
)
139155
print(f"Received unexpected response type: {response.get('type')}")
140156

157+
141158
if __name__ == "__main__":
142159
try:
143160
demo = WeatherAgentDemo()
144161
demo.run_demo()
145162
except Exception as e:
146-
logger.error(f"Demo failed unexpectedly: {e}", exc_info=True) # Add traceback info
163+
logger.error(
164+
f"Demo failed unexpectedly: {e}", exc_info=True
165+
) # Add traceback info

instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@ def loader():
3232

3333

3434
_KNOWN_EXTENSIONS = {
35+
"bedrock-agent-runtime": _lazy_load(
36+
".bedrock", "_BedrockRuntimeExtension"
37+
),
3538
"bedrock-runtime": _lazy_load(".bedrock", "_BedrockRuntimeExtension"),
36-
"bedrock-agent-runtime": _lazy_load(".bedrock", "_BedrockRuntimeExtension"),
3739
"dynamodb": _lazy_load(".dynamodb", "_DynamoDbExtension"),
3840
"lambda": _lazy_load(".lmbd", "_LambdaExtension"),
3941
"sns": _lazy_load(".sns", "_SnsExtension"),

instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/bedrock.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545
ERROR_TYPE,
4646
)
4747
from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import (
48+
GEN_AI_AGENT_ID,
49+
GEN_AI_AGENT_NAME,
4850
GEN_AI_OPERATION_NAME,
4951
GEN_AI_REQUEST_MAX_TOKENS,
5052
GEN_AI_REQUEST_MODEL,
@@ -54,18 +56,15 @@
5456
GEN_AI_RESPONSE_FINISH_REASONS,
5557
GEN_AI_SYSTEM,
5658
GEN_AI_TOKEN_TYPE,
57-
GEN_AI_USAGE_INPUT_TOKENS,
58-
GEN_AI_USAGE_OUTPUT_TOKENS,
59-
GEN_AI_AGENT_ID,
60-
GEN_AI_AGENT_NAME,
6159
GEN_AI_TOOL_CALL_ID,
6260
GEN_AI_TOOL_NAME,
6361
GEN_AI_TOOL_TYPE,
62+
GEN_AI_USAGE_INPUT_TOKENS,
63+
GEN_AI_USAGE_OUTPUT_TOKENS,
6464
GenAiOperationNameValues,
6565
GenAiSystemValues,
6666
GenAiTokenTypeValues,
6767
)
68-
6968
from opentelemetry.semconv._incubating.metrics.gen_ai_metrics import (
7069
GEN_AI_CLIENT_OPERATION_DURATION,
7170
GEN_AI_CLIENT_TOKEN_USAGE,
@@ -189,7 +188,9 @@ def extract_attributes(self, attributes: _AttributeMapT):
189188
agent_alias_id = self._call_context.params.get("agentAliasId")
190189

191190
self._set_if_not_none(attributes, GEN_AI_AGENT_ID, agent_id)
192-
self._set_if_not_none(attributes, GEN_AI_AGENT_NAME, agent_alias_id)
191+
self._set_if_not_none(
192+
attributes, GEN_AI_AGENT_NAME, agent_alias_id
193+
)
193194
return
194195

195196
# Handle non-agent chat completions
@@ -506,7 +507,9 @@ def _invoke_agent_on_success(
506507
instrumentor_context: _BotocoreInstrumentorContext,
507508
):
508509
try:
509-
if "completion" in result and isinstance(result["completion"], EventStream):
510+
if "completion" in result and isinstance(
511+
result["completion"], EventStream
512+
):
510513
event_stream = result["completion"]
511514

512515
# Drain the stream so we can instrument AND keep events
@@ -522,15 +525,15 @@ def _invoke_agent_on_success(
522525
# Record metrics
523526
metrics = instrumentor_context.metrics
524527
metrics_attributes = self._extract_metrics_attributes()
525-
if operation_duration_histogram := metrics.get(GEN_AI_CLIENT_OPERATION_DURATION):
528+
if operation_duration_histogram := metrics.get(
529+
GEN_AI_CLIENT_OPERATION_DURATION
530+
):
526531
duration = max((default_timer() - self._operation_start), 0)
527532
operation_duration_histogram.record(
528533
duration,
529534
attributes=metrics_attributes,
530535
)
531536

532-
except json.JSONDecodeError:
533-
_logger.debug("Error: Unable to parse the response body as JSON")
534537
except Exception as exc: # pylint: disable=broad-exception-caught
535538
_logger.debug("Error processing response: %s", exc)
536539

@@ -548,7 +551,9 @@ def _handle_return_control(self, span: Span, event: dict):
548551
func_input = input_item["functionInvocationInput"]
549552
action_group = func_input.get("actionGroup")
550553
function = func_input.get("function")
551-
span.set_attribute(GEN_AI_TOOL_NAME, action_group)
554+
span.set_attribute(
555+
GEN_AI_TOOL_NAME, action_group + "." + function
556+
)
552557
span.set_attribute(GEN_AI_TOOL_TYPE, "function")
553558

554559
# Handle API invocation
@@ -847,6 +852,7 @@ def on_error(
847852
attributes=metrics_attributes,
848853
)
849854

855+
850856
def _replay_events(events):
851857
"""
852858
Helper so that user can still iterate EventStream

instrumentation/opentelemetry-instrumentation-botocore/test-requirements-0.txt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,3 @@ zipp==3.19.2
3737
-e opentelemetry-instrumentation
3838
-e propagator/opentelemetry-propagator-aws-xray
3939
-e instrumentation/opentelemetry-instrumentation-botocore
40-
git+https://github.com/open-telemetry/opentelemetry-python.git@main#subdirectory=opentelemetry-semantic-conventions
41-

instrumentation/opentelemetry-instrumentation-botocore/test-requirements-1.txt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,3 @@ zipp==3.19.2
3737
-e opentelemetry-instrumentation
3838
-e propagator/opentelemetry-propagator-aws-xray
3939
-e instrumentation/opentelemetry-instrumentation-botocore
40-
git+https://github.com/open-telemetry/opentelemetry-python.git@main#subdirectory=opentelemetry-semantic-conventions
41-

instrumentation/opentelemetry-instrumentation-botocore/tests/bedrock_utils.py

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -273,31 +273,52 @@ def assert_message_in_logs(log, event_name, expected_content, parent_span):
273273
assert_log_parent(log, parent_span)
274274

275275

276-
def assert_invoke_agent_attributes(span, agent_id, agent_alias_id, session_id, has_tool_call=False, is_result_call=False):
276+
def assert_invoke_agent_attributes(
277+
span,
278+
agent_id,
279+
agent_alias_id,
280+
session_id,
281+
has_tool_call=False,
282+
is_result_call=False,
283+
):
277284
# Check system and operation name
278-
assert span.attributes.get(GenAIAttributes.GEN_AI_SYSTEM) == GenAIAttributes.GenAiSystemValues.AWS_BEDROCK.value
279-
assert span.attributes.get(GenAIAttributes.GEN_AI_OPERATION_NAME) == "invoke_agent"
285+
assert (
286+
span.attributes.get(GenAIAttributes.GEN_AI_SYSTEM)
287+
== GenAIAttributes.GenAiSystemValues.AWS_BEDROCK.value
288+
)
289+
assert (
290+
span.attributes.get(GenAIAttributes.GEN_AI_OPERATION_NAME)
291+
== "invoke_agent"
292+
)
280293

281294
# Check agent attributes
282295
assert span.attributes.get(GenAIAttributes.GEN_AI_AGENT_ID) == agent_id
283-
assert span.attributes.get(GenAIAttributes.GEN_AI_AGENT_NAME) == agent_alias_id
296+
assert (
297+
span.attributes.get(GenAIAttributes.GEN_AI_AGENT_NAME)
298+
== agent_alias_id
299+
)
284300

285301
# If tool call exists, check tool attributes
286302
if has_tool_call:
287303
assert GenAIAttributes.GEN_AI_TOOL_CALL_ID in span.attributes
288304
assert GenAIAttributes.GEN_AI_TOOL_NAME in span.attributes
289305
assert GenAIAttributes.GEN_AI_TOOL_TYPE in span.attributes
290306
allowed_tool_types = {"extension", "function", "datastore"}
291-
assert span.attributes.get(GenAIAttributes.GEN_AI_TOOL_TYPE) in allowed_tool_types, \
292-
f"Unexpected tool type in span: {span.attributes.get(GenAIAttributes.GEN_AI_TOOL_TYPE)}"
307+
assert (
308+
span.attributes.get(GenAIAttributes.GEN_AI_TOOL_TYPE)
309+
in allowed_tool_types
310+
), f"Unexpected tool type in span: {span.attributes.get(GenAIAttributes.GEN_AI_TOOL_TYPE)}"
293311
elif is_result_call:
294312
assert GenAIAttributes.GEN_AI_TOOL_CALL_ID not in span.attributes
295313
assert GenAIAttributes.GEN_AI_TOOL_NAME not in span.attributes
296314
assert GenAIAttributes.GEN_AI_TOOL_TYPE not in span.attributes
297315

298316

299317
def assert_all_metric_attributes(
300-
data_point, operation_name: str, model: str, error_type: str | None = None
318+
data_point,
319+
operation_name: str,
320+
model: str | None,
321+
error_type: str | None = None,
301322
):
302323
assert GenAIAttributes.GEN_AI_OPERATION_NAME in data_point.attributes
303324
assert (
@@ -311,7 +332,10 @@ def assert_all_metric_attributes(
311332
)
312333
if model is not None:
313334
assert GenAIAttributes.GEN_AI_REQUEST_MODEL in data_point.attributes
314-
assert data_point.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == model
335+
assert (
336+
data_point.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL]
337+
== model
338+
)
315339

316340
if error_type is not None:
317341
assert ERROR_TYPE in data_point.attributes

instrumentation/opentelemetry-instrumentation-botocore/tests/conftest.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,10 @@ def fixture_meter_provider(metric_reader):
7777
def bedrock_runtime_client():
7878
return boto3.client("bedrock-runtime")
7979

80+
8081
@pytest.fixture
8182
def bedrock_agent_client():
82-
return boto3.client('bedrock-agent-runtime')
83+
return boto3.client("bedrock-agent-runtime")
8384

8485

8586
@pytest.fixture(autouse=True)

0 commit comments

Comments
 (0)