Skip to content

Commit 0d05ca7

Browse files
authored
Add bedrock contract tests. (#227)
The PR is a follow up on bedrock service support PR: #209 We add contract tests for following Bedrock services that covers all resource attributes we newly support: 1. Bedrock API: `GetGuardrail` 2. BedrockAgent APIs: `GetAgent`, `GetDataSource`, `GetKnowledgeBase` 3. BedrockRuntime API: `InvokeModel` 4. BedrockAgentRuntime API: `InvokeAgent` Upgrade `botocore` and `boto3` to the latest version `1.34.143` so that to support Bedrock services API calls. Upgrade `localstack/localstack` image to the latest version `3.5.0` so resolve the SQS API call issue using `localstack/localstack:2.0.1` with new version of `boto3`: localstack/localstack#9610 **Contract test limitation:** The contract tests in current repo is using [LocalStackContainer](https://github.com/aws-observability/aws-otel-python-instrumentation/blob/912dd93ff19b7a4594bb9ed1a7d8cde4907a735d/contract-tests/tests/test/amazon/botocore/botocore_test.py#L68) to serve AWS SDK service calls. But it doesn’t has bedrock related service support (This is [the full service list](https://docs.localstack.cloud/references/coverage/) it support.). In this case, no matter which bedrock API we call in contract test, the response will always be 4XX. As a workaround, we inject `inject_200_success` and `inject_500_error` directly into the API call to make sure we receive http response with expected status code and attributes. **`_assert_semantic_conventions_span_attributes` function change:** In [_assert_semantic_conventions_span_attributes function](https://github.com/aws-observability/aws-otel-python-instrumentation/blob/1753bbf2c3cd41778abc358a7f1e3199e48c7fa9/contract-tests/tests/test/amazon/botocore/botocore_test.py#L448) it is checking the input `service` equals to `rpc.service`, however, we pass the input service with ["remote_service"](https://github.com/aws-observability/aws-otel-python-instrumentation/blob/1753bbf2c3cd41778abc358a7f1e3199e48c7fa9/contract-tests/tests/test/amazon/botocore/botocore_test.py#L430), where there is mismatch for example for Bedrock Agent Runtime: we have `rpc_service="Bedrock Agent Runtime", remote_service="AWS::Bedrock". ` Thus we change to use `rpc_service` if it is provided by: ``` kwargs.get("rpc_service") if "rpc_service" in kwargs else kwargs.get("remote_service").split("::")[-1], ``` By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent 1d0aedd commit 0d05ca7

File tree

3 files changed

+255
-4
lines changed

3 files changed

+255
-4
lines changed

contract-tests/images/applications/botocore/botocore_server.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
# SPDX-License-Identifier: Apache-2.0
33
import atexit
4+
import json
45
import os
56
import tempfile
7+
from collections import namedtuple
68
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
79
from threading import Thread
810

@@ -41,6 +43,8 @@ def do_GET(self):
4143
self._handle_sqs_request()
4244
if self.in_path("kinesis"):
4345
self._handle_kinesis_request()
46+
if self.in_path("bedrock"):
47+
self._handle_bedrock_request()
4448

4549
self._end_request(self.main_status)
4650

@@ -203,6 +207,100 @@ def _handle_kinesis_request(self) -> None:
203207
else:
204208
set_main_status(404)
205209

210+
def _handle_bedrock_request(self) -> None:
211+
# Localstack does not support Bedrock related services.
212+
# we inject inject_200_success directly into the API call
213+
# to make sure we receive http response with expected status code and attributes.
214+
bedrock_client: BaseClient = boto3.client("bedrock", endpoint_url=_AWS_SDK_ENDPOINT, region_name=_AWS_REGION)
215+
bedrock_agent_client: BaseClient = boto3.client(
216+
"bedrock-agent", endpoint_url=_AWS_SDK_ENDPOINT, region_name=_AWS_REGION
217+
)
218+
bedrock_runtime_client: BaseClient = boto3.client(
219+
"bedrock-runtime", endpoint_url=_AWS_SDK_ENDPOINT, region_name=_AWS_REGION
220+
)
221+
bedrock_agent_runtime_client: BaseClient = boto3.client(
222+
"bedrock-agent-runtime", endpoint_url=_AWS_SDK_ENDPOINT, region_name=_AWS_REGION
223+
)
224+
if self.in_path("getknowledgebase/get_knowledge_base"):
225+
set_main_status(200)
226+
bedrock_agent_client.meta.events.register(
227+
"before-call.bedrock-agent.GetKnowledgeBase",
228+
inject_200_success,
229+
)
230+
bedrock_agent_client.get_knowledge_base(knowledgeBaseId="invalid-knowledge-base-id")
231+
elif self.in_path("getdatasource/get_data_source"):
232+
set_main_status(200)
233+
bedrock_agent_client.meta.events.register(
234+
"before-call.bedrock-agent.GetDataSource",
235+
inject_200_success,
236+
)
237+
bedrock_agent_client.get_data_source(knowledgeBaseId="TESTKBSEID", dataSourceId="DATASURCID")
238+
elif self.in_path("getagent/get-agent"):
239+
set_main_status(200)
240+
bedrock_agent_client.meta.events.register(
241+
"before-call.bedrock-agent.GetAgent",
242+
inject_200_success,
243+
)
244+
bedrock_agent_client.get_agent(agentId="TESTAGENTID")
245+
elif self.in_path("getguardrail/get-guardrail"):
246+
set_main_status(200)
247+
bedrock_client.meta.events.register(
248+
"before-call.bedrock.GetGuardrail",
249+
lambda **kwargs: inject_200_success(guardrailId="bt4o77i015cu", **kwargs),
250+
)
251+
bedrock_client.get_guardrail(
252+
guardrailIdentifier="arn:aws:bedrock:us-east-1:000000000000:guardrail/bt4o77i015cu"
253+
)
254+
elif self.in_path("invokeagent/invoke_agent"):
255+
set_main_status(200)
256+
bedrock_agent_runtime_client.meta.events.register(
257+
"before-call.bedrock-agent-runtime.InvokeAgent",
258+
inject_200_success,
259+
)
260+
bedrock_agent_runtime_client.invoke_agent(
261+
agentId="Q08WFRPHVL",
262+
agentAliasId="testAlias",
263+
sessionId="testSessionId",
264+
inputText="Invoke agent sample input text",
265+
)
266+
elif self.in_path("retrieve/retrieve"):
267+
set_main_status(200)
268+
bedrock_agent_runtime_client.meta.events.register(
269+
"before-call.bedrock-agent-runtime.Retrieve",
270+
inject_200_success,
271+
)
272+
bedrock_agent_runtime_client.retrieve(
273+
knowledgeBaseId="test-knowledge-base-id",
274+
retrievalQuery={
275+
"text": "an example of retrieve query",
276+
},
277+
)
278+
elif self.in_path("invokemodel/invoke-model"):
279+
set_main_status(200)
280+
bedrock_runtime_client.meta.events.register(
281+
"before-call.bedrock-runtime.InvokeModel",
282+
inject_200_success,
283+
)
284+
model_id = "amazon.titan-text-premier-v1:0"
285+
user_message = "Describe the purpose of a 'hello world' program in one line."
286+
prompt = f"<s>[INST] {user_message} [/INST]"
287+
body = json.dumps(
288+
{
289+
"inputText": prompt,
290+
"textGenerationConfig": {
291+
"maxTokenCount": 3072,
292+
"stopSequences": [],
293+
"temperature": 0.7,
294+
"topP": 0.9,
295+
},
296+
}
297+
)
298+
accept = "application/json"
299+
content_type = "application/json"
300+
bedrock_runtime_client.invoke_model(body=body, modelId=model_id, accept=accept, contentType=content_type)
301+
else:
302+
set_main_status(404)
303+
206304
def _end_request(self, status_code: int):
207305
self.send_response_only(status_code)
208306
self.end_headers()
@@ -251,6 +349,28 @@ def prepare_aws_server() -> None:
251349
print("Unexpected exception occurred", exception)
252350

253351

352+
def inject_200_success(**kwargs):
353+
response_metadata = {
354+
"HTTPStatusCode": 200,
355+
"RequestId": "mock-request-id",
356+
}
357+
358+
response_body = {
359+
"Message": "Request succeeded",
360+
"ResponseMetadata": response_metadata,
361+
}
362+
363+
guardrail_id = kwargs.get("guardrailId")
364+
if guardrail_id is not None:
365+
response_body["guardrailId"] = guardrail_id
366+
367+
HTTPResponse = namedtuple("HTTPResponse", ["status_code", "headers", "body"])
368+
headers = kwargs.get("headers", {})
369+
body = kwargs.get("body", "")
370+
http_response = HTTPResponse(200, headers=headers, body=body)
371+
return http_response, response_body
372+
373+
254374
def main() -> None:
255375
prepare_aws_server()
256376
server_address: Tuple[str, int] = ("0.0.0.0", _PORT)
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
opentelemetry-distro==0.46b0
22
opentelemetry-exporter-otlp-proto-grpc==1.25.0
33
typing-extensions==4.9.0
4-
botocore==1.34.26
5-
boto3==1.34.26
4+
botocore==1.34.143
5+
boto3==1.34.143

contract-tests/tests/test/amazon/botocore/botocore_test.py

Lines changed: 133 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@
2929
_AWS_SQS_QUEUE_URL: str = "aws.sqs.queue.url"
3030
_AWS_SQS_QUEUE_NAME: str = "aws.sqs.queue.name"
3131
_AWS_KINESIS_STREAM_NAME: str = "aws.kinesis.stream.name"
32+
_AWS_BEDROCK_AGENT_ID: str = "aws.bedrock.agent.id"
33+
_AWS_BEDROCK_GUARDRAIL_ID: str = "aws.bedrock.guardrail.id"
34+
_AWS_BEDROCK_KNOWLEDGE_BASE_ID: str = "aws.bedrock.knowledge_base.id"
35+
_AWS_BEDROCK_DATA_SOURCE_ID: str = "aws.bedrock.data_source.id"
36+
_GEN_AI_REQUEST_MODEL: str = "gen_ai.request.model"
3237

3338

3439
# pylint: disable=too-many-public-methods
@@ -66,7 +71,7 @@ def set_up_dependency_container(cls):
6671
)
6772
}
6873
cls._local_stack: LocalStackContainer = (
69-
LocalStackContainer(image="localstack/localstack:2.0.1")
74+
LocalStackContainer(image="localstack/localstack:3.5.0")
7075
.with_name("localstack")
7176
.with_services("s3", "sqs", "dynamodb", "kinesis")
7277
.with_env("DEFAULT_REGION", "us-west-2")
@@ -372,6 +377,132 @@ def test_kinesis_fault(self):
372377
span_name="Kinesis.PutRecord",
373378
)
374379

380+
def test_bedrock_runtime_invoke_model(self):
381+
self.do_test_requests(
382+
"bedrock/invokemodel/invoke-model",
383+
"GET",
384+
200,
385+
0,
386+
0,
387+
rpc_service="Bedrock Runtime",
388+
remote_service="AWS::BedrockRuntime",
389+
remote_operation="InvokeModel",
390+
remote_resource_type="AWS::Bedrock::Model",
391+
remote_resource_identifier="amazon.titan-text-premier-v1:0",
392+
request_specific_attributes={
393+
_GEN_AI_REQUEST_MODEL: "amazon.titan-text-premier-v1:0",
394+
},
395+
span_name="Bedrock Runtime.InvokeModel",
396+
)
397+
398+
def test_bedrock_get_guardrail(self):
399+
self.do_test_requests(
400+
"bedrock/getguardrail/get-guardrail",
401+
"GET",
402+
200,
403+
0,
404+
0,
405+
rpc_service="Bedrock",
406+
remote_service="AWS::Bedrock",
407+
remote_operation="GetGuardrail",
408+
remote_resource_type="AWS::Bedrock::Guardrail",
409+
remote_resource_identifier="bt4o77i015cu",
410+
request_specific_attributes={
411+
_AWS_BEDROCK_GUARDRAIL_ID: "bt4o77i015cu",
412+
},
413+
span_name="Bedrock.GetGuardrail",
414+
)
415+
416+
def test_bedrock_agent_runtime_invoke_agent(self):
417+
self.do_test_requests(
418+
"bedrock/invokeagent/invoke_agent",
419+
"GET",
420+
200,
421+
0,
422+
0,
423+
rpc_service="Bedrock Agent Runtime",
424+
remote_service="AWS::Bedrock",
425+
remote_operation="InvokeAgent",
426+
remote_resource_type="AWS::Bedrock::Agent",
427+
remote_resource_identifier="Q08WFRPHVL",
428+
request_specific_attributes={
429+
_AWS_BEDROCK_AGENT_ID: "Q08WFRPHVL",
430+
},
431+
span_name="Bedrock Agent Runtime.InvokeAgent",
432+
)
433+
434+
def test_bedrock_agent_runtime_retrieve(self):
435+
self.do_test_requests(
436+
"bedrock/retrieve/retrieve",
437+
"GET",
438+
200,
439+
0,
440+
0,
441+
rpc_service="Bedrock Agent Runtime",
442+
remote_service="AWS::Bedrock",
443+
remote_operation="Retrieve",
444+
remote_resource_type="AWS::Bedrock::KnowledgeBase",
445+
remote_resource_identifier="test-knowledge-base-id",
446+
request_specific_attributes={
447+
_AWS_BEDROCK_KNOWLEDGE_BASE_ID: "test-knowledge-base-id",
448+
},
449+
span_name="Bedrock Agent Runtime.Retrieve",
450+
)
451+
452+
def test_bedrock_agent_get_agent(self):
453+
self.do_test_requests(
454+
"bedrock/getagent/get-agent",
455+
"GET",
456+
200,
457+
0,
458+
0,
459+
rpc_service="Bedrock Agent",
460+
remote_service="AWS::Bedrock",
461+
remote_operation="GetAgent",
462+
remote_resource_type="AWS::Bedrock::Agent",
463+
remote_resource_identifier="TESTAGENTID",
464+
request_specific_attributes={
465+
_AWS_BEDROCK_AGENT_ID: "TESTAGENTID",
466+
},
467+
span_name="Bedrock Agent.GetAgent",
468+
)
469+
470+
def test_bedrock_agent_get_knowledge_base(self):
471+
self.do_test_requests(
472+
"bedrock/getknowledgebase/get_knowledge_base",
473+
"GET",
474+
200,
475+
0,
476+
0,
477+
rpc_service="Bedrock Agent",
478+
remote_service="AWS::Bedrock",
479+
remote_operation="GetKnowledgeBase",
480+
remote_resource_type="AWS::Bedrock::KnowledgeBase",
481+
remote_resource_identifier="invalid-knowledge-base-id",
482+
request_specific_attributes={
483+
_AWS_BEDROCK_KNOWLEDGE_BASE_ID: "invalid-knowledge-base-id",
484+
},
485+
span_name="Bedrock Agent.GetKnowledgeBase",
486+
)
487+
488+
def test_bedrock_agent_get_data_source(self):
489+
self.do_test_requests(
490+
"bedrock/getdatasource/get_data_source",
491+
"GET",
492+
200,
493+
0,
494+
0,
495+
rpc_service="Bedrock Agent",
496+
remote_service="AWS::Bedrock",
497+
remote_operation="GetDataSource",
498+
remote_resource_type="AWS::Bedrock::DataSource",
499+
remote_resource_identifier="DATASURCID",
500+
request_specific_attributes={
501+
_AWS_BEDROCK_DATA_SOURCE_ID: "DATASURCID",
502+
},
503+
span_name="Bedrock Agent.GetDataSource",
504+
)
505+
375506
@override
376507
def _assert_aws_span_attributes(self, resource_scope_spans: List[ResourceScopeSpan], path: str, **kwargs) -> None:
377508
target_spans: List[Span] = []
@@ -427,7 +558,7 @@ def _assert_semantic_conventions_span_attributes(
427558
self.assertEqual(target_spans[0].name, kwargs.get("span_name"))
428559
self._assert_semantic_conventions_attributes(
429560
target_spans[0].attributes,
430-
kwargs.get("remote_service"),
561+
kwargs.get("rpc_service") if "rpc_service" in kwargs else kwargs.get("remote_service").split("::")[-1],
431562
kwargs.get("remote_operation"),
432563
status_code,
433564
kwargs.get("request_specific_attributes", {}),

0 commit comments

Comments
 (0)