Skip to content

Commit d6866c4

Browse files
authored
feat(bedrock): added support for tool results, definitions (#14371)
This PR adds improved tracking of tool results and definitions to the LLMObs Bedrock integration ## Checklist - [x] PR author has checked that all the criteria below are met - The PR description includes an overview of the change - The PR description articulates the motivation for the change - The change includes tests OR the PR description describes a testing strategy - The PR description notes risks associated with the change, if any - Newly-added code is easy to change - The change follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) - The change includes or references documentation updates if necessary - Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [x] Reviewer has checked that all the criteria below are met - Title is accurate - All changes are related to the pull request's stated goal - Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes - Testing strategy adequately addresses listed risks - Newly-added code is easy to change - Release note makes sense to a user of the library - If necessary, author has acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment - Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)
1 parent fd87333 commit d6866c4

File tree

6 files changed

+89
-27
lines changed

6 files changed

+89
-27
lines changed

ddtrace/contrib/internal/botocore/services/bedrock.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,12 +183,14 @@ def _extract_request_params_for_converse(params: Dict[str, Any]) -> Dict[str, An
183183
if system_content_block:
184184
prompt.append({"role": "system", "content": system_content_block})
185185
prompt += messages
186+
tool_config = params.get("toolConfig", {})
186187
return {
187188
"prompt": prompt,
188189
"temperature": inference_config.get("temperature", ""),
189190
"top_p": inference_config.get("topP", ""),
190191
"max_tokens": inference_config.get("maxTokens", ""),
191192
"stop_sequences": inference_config.get("stopSequences", []),
193+
"tool_config": tool_config,
192194
}
193195

194196

ddtrace/llmobs/_integrations/bedrock.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from ddtrace.llmobs._constants import PROXY_REQUEST
2424
from ddtrace.llmobs._constants import SPAN_KIND
2525
from ddtrace.llmobs._constants import TAGS
26+
from ddtrace.llmobs._constants import TOOL_DEFINITIONS
2627
from ddtrace.llmobs._integrations import BaseLLMIntegration
2728
from ddtrace.llmobs._integrations.bedrock_agents import _create_or_update_bedrock_trace_step_span
2829
from ddtrace.llmobs._integrations.bedrock_agents import _extract_trace_step_id
@@ -32,7 +33,9 @@
3233
from ddtrace.llmobs._integrations.utils import get_messages_from_converse_content
3334
from ddtrace.llmobs._integrations.utils import update_proxy_workflow_input_output_value
3435
from ddtrace.llmobs._telemetry import record_bedrock_agent_span_event_created
36+
from ddtrace.llmobs._utils import _get_attr
3537
from ddtrace.llmobs._writer import LLMObsSpanEvent
38+
from ddtrace.llmobs.utils import ToolDefinition
3639
from ddtrace.trace import Span
3740

3841

@@ -97,6 +100,10 @@ def _llmobs_set_tags(
97100
metadata["max_tokens"] = int(request_params.get("max_tokens") or 0)
98101

99102
prompt = request_params.get("prompt", "")
103+
tool_config = request_params.get("tool_config", {})
104+
tool_definitions = self._extract_tool_definitions(tool_config)
105+
if tool_definitions:
106+
span._set_ctx_item(TOOL_DEFINITIONS, tool_definitions)
100107

101108
is_converse = ctx["resource"] in ("Converse", "ConverseStream")
102109
input_messages = (
@@ -381,3 +388,17 @@ def _tag_proxy_request(self, ctx: core.ExecutionContext) -> None:
381388
base_url = self._get_base_url(instance=ctx.get_item("instance"))
382389
if self._is_instrumented_proxy_url(base_url):
383390
ctx.set_item(PROXY_REQUEST, True)
391+
392+
def _extract_tool_definitions(self, tool_config: Dict[str, Any]) -> List[ToolDefinition]:
393+
"""Extract tool definitions from the stored tool config."""
394+
tools = _get_attr(tool_config, "tools", [])
395+
tool_definitions = []
396+
for tool in tools:
397+
tool_spec = _get_attr(tool, "toolSpec", {})
398+
tool_definition_info = ToolDefinition(
399+
name=_get_attr(tool_spec, "name", ""),
400+
description=_get_attr(tool_spec, "description", ""),
401+
schema=_get_attr(tool_spec, "inputSchema", {}),
402+
)
403+
tool_definitions.append(tool_definition_info)
404+
return tool_definitions

ddtrace/llmobs/_integrations/utils.py

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
from ddtrace.llmobs._utils import _get_attr
2828
from ddtrace.llmobs._utils import load_data_value
2929
from ddtrace.llmobs._utils import safe_json
30+
from ddtrace.llmobs.utils import ToolCall
31+
from ddtrace.llmobs.utils import ToolResult
3032

3133

3234
try:
@@ -232,23 +234,25 @@ def get_messages_from_converse_content(role: str, content: List[Dict[str, Any]])
232234
"""
233235
if not content or not isinstance(content, list) or not isinstance(content[0], dict):
234236
return []
235-
messages: List[Dict[str, Union[str, List[Dict[str, Any]]]]] = []
237+
messages: List[Dict[str, Union[str, List[Dict[str, Any]], List[ToolCall], List[ToolResult]]]] = []
236238
content_blocks = []
237239
tool_calls_info = []
238240
tool_messages: List[Dict[str, Any]] = []
239-
unsupported_content_messages: List[Dict[str, Union[str, List[Dict[str, Any]]]]] = []
241+
unsupported_content_messages: List[
242+
Dict[str, Union[str, List[Dict[str, Any]], List[ToolCall], List[ToolResult]]]
243+
] = []
240244
for content_block in content:
241245
if content_block.get("text") and isinstance(content_block.get("text"), str):
242246
content_blocks.append(content_block.get("text", ""))
243247
elif content_block.get("toolUse") and isinstance(content_block.get("toolUse"), dict):
244248
toolUse = content_block.get("toolUse", {})
245-
tool_calls_info.append(
246-
{
247-
"name": str(toolUse.get("name", "")),
248-
"arguments": toolUse.get("input", {}),
249-
"tool_id": str(toolUse.get("toolUseId", "")),
250-
}
249+
tool_call_info = ToolCall(
250+
name=str(toolUse.get("name", "")),
251+
arguments=toolUse.get("input", {}),
252+
tool_id=str(toolUse.get("toolUseId", "")),
253+
type="toolUse",
251254
)
255+
tool_calls_info.append(tool_call_info)
252256
elif content_block.get("toolResult") and isinstance(content_block.get("toolResult"), dict):
253257
tool_message: Dict[str, Any] = content_block.get("toolResult", {})
254258
tool_message_contents: List[Dict[str, Any]] = tool_message.get("content", [])
@@ -258,21 +262,25 @@ def get_messages_from_converse_content(role: str, content: List[Dict[str, Any]])
258262
tool_message_content_text: Optional[str] = tool_message_content.get("text")
259263
tool_message_content_json: Optional[Dict[str, Any]] = tool_message_content.get("json")
260264

265+
tool_result_info = ToolResult(
266+
result=tool_message_content_text
267+
or (tool_message_content_json and safe_json(tool_message_content_json))
268+
or f"[Unsupported content type(s): {','.join(tool_message_content.keys())}]",
269+
tool_id=tool_message_id,
270+
type="toolResult",
271+
)
261272
tool_messages.append(
262273
{
263-
"content": tool_message_content_text
264-
or (tool_message_content_json and safe_json(tool_message_content_json))
265-
or f"[Unsupported content type(s): {','.join(tool_message_content.keys())}]",
266-
"role": "tool",
267-
"tool_id": tool_message_id,
274+
"tool_results": [tool_result_info],
275+
"role": "user",
268276
}
269277
)
270278
else:
271279
content_type = ",".join(content_block.keys())
272280
unsupported_content_messages.append(
273281
{"content": "[Unsupported content type: {}]".format(content_type), "role": role}
274282
)
275-
message = {} # type: dict[str, Union[str, list[dict[str, dict]]]]
283+
message: Dict[str, Union[str, List[Dict[str, Any]], List[ToolCall], List[ToolResult]]] = {}
276284
if tool_calls_info:
277285
message.update({"tool_calls": tool_calls_info})
278286
if content_blocks:
@@ -1005,9 +1013,9 @@ def llmobs_output_messages(self) -> Tuple[List[Dict[str, Any]], List[Tuple[str,
10051013
"tool_calls": [
10061014
{
10071015
"tool_id": item.call_id,
1008-
"arguments": json.loads(item.arguments)
1009-
if isinstance(item.arguments, str)
1010-
else item.arguments,
1016+
"arguments": (
1017+
json.loads(item.arguments) if isinstance(item.arguments, str) else item.arguments
1018+
),
10111019
"name": getattr(item, "name", ""),
10121020
"type": getattr(item, "type", "function"),
10131021
}
@@ -1119,19 +1127,20 @@ def get_final_message_converse_stream_message(
11191127
tool_block = tool_blocks.get(idx)
11201128
if not tool_block:
11211129
continue
1122-
tool_call = {
1123-
"name": tool_block.get("name", ""),
1124-
"tool_id": tool_block.get("toolUseId", ""),
1125-
}
11261130
tool_input = tool_block.get("input")
1131+
tool_args = {}
11271132
if tool_input is not None:
1128-
tool_args = {}
11291133
try:
11301134
tool_args = json.loads(tool_input)
11311135
except (json.JSONDecodeError, ValueError):
11321136
tool_args = {"input": tool_input}
1133-
tool_call.update({"arguments": tool_args} if tool_args else {})
1134-
tool_calls.append(tool_call)
1137+
tool_call_info = ToolCall(
1138+
name=tool_block.get("name", ""),
1139+
tool_id=tool_block.get("toolUseId", ""),
1140+
arguments=tool_args if tool_args else {},
1141+
type="toolUse",
1142+
)
1143+
tool_calls.append(tool_call_info)
11351144

11361145
if tool_calls:
11371146
message_output["tool_calls"] = tool_calls
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
features:
2+
- |
3+
LLM Observability: Adds support for collecting tool definitions, tool calls and tool results in the Amazon Bedrock integration.

tests/contrib/botocore/bedrock_utils.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,19 @@
2525
"luxury 4/5 star resorts)"
2626
)
2727

28+
FETCH_CONCEPT_TOOL_DEFINITION = {
29+
"name": "fetch_concept",
30+
"description": "Fetch an expert explanation for a concept",
31+
"schema": {
32+
"json": {
33+
"type": "object",
34+
"properties": {"concept": {"type": "string", "description": "The concept to explain"}},
35+
"required": ["concept"],
36+
},
37+
},
38+
}
39+
40+
2841
bedrock_converse_args_with_system_and_tool = {
2942
"system": "You are an expert swe that is to use the tool fetch_concept",
3043
"user_message": "Explain the concept of distributed tracing in a simple way",

tests/contrib/botocore/test_bedrock_llmobs.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from tests.contrib.botocore.bedrock_utils import _MODELS
1111
from tests.contrib.botocore.bedrock_utils import _REQUEST_BODIES
1212
from tests.contrib.botocore.bedrock_utils import BOTO_VERSION
13+
from tests.contrib.botocore.bedrock_utils import FETCH_CONCEPT_TOOL_DEFINITION
1314
from tests.contrib.botocore.bedrock_utils import bedrock_converse_args_with_system_and_tool
1415
from tests.contrib.botocore.bedrock_utils import create_bedrock_converse_request
1516
from tests.contrib.botocore.bedrock_utils import get_mock_response_data
@@ -268,6 +269,7 @@ def test_llmobs_converse(cls, bedrock_client, request_vcr, mock_tracer, llmobs_e
268269
"arguments": {"concept": "distributed tracing"},
269270
"name": "fetch_concept",
270271
"tool_id": mock.ANY,
272+
"type": "toolUse",
271273
}
272274
],
273275
}
@@ -282,6 +284,7 @@ def test_llmobs_converse(cls, bedrock_client, request_vcr, mock_tracer, llmobs_e
282284
"output_tokens": response["usage"]["outputTokens"],
283285
"total_tokens": response["usage"]["totalTokens"],
284286
},
287+
tool_definitions=[FETCH_CONCEPT_TOOL_DEFINITION],
285288
tags={"service": "aws.bedrock-runtime", "ml_app": "<ml-app-name>"},
286289
)
287290

@@ -346,6 +349,7 @@ def test_llmobs_converse_stream(cls, bedrock_client, request_vcr, mock_tracer, l
346349
"arguments": {"concept": "distributed tracing"},
347350
"name": "fetch_concept",
348351
"tool_id": mock.ANY,
352+
"type": "toolUse",
349353
}
350354
],
351355
}
@@ -359,6 +363,7 @@ def test_llmobs_converse_stream(cls, bedrock_client, request_vcr, mock_tracer, l
359363
"output_tokens": 64,
360364
"total_tokens": 323,
361365
},
366+
tool_definitions=[FETCH_CONCEPT_TOOL_DEFINITION],
362367
tags={"service": "aws.bedrock-runtime", "ml_app": "<ml-app-name>"},
363368
)
364369

@@ -398,6 +403,7 @@ def test_llmobs_converse_modified_stream(cls, bedrock_client, request_vcr, mock_
398403
"arguments": {"concept": "distributed tracing"},
399404
"name": "fetch_concept",
400405
"tool_id": mock.ANY,
406+
"type": "toolUse",
401407
}
402408
],
403409
}
@@ -411,6 +417,7 @@ def test_llmobs_converse_modified_stream(cls, bedrock_client, request_vcr, mock_
411417
"output_tokens": 64,
412418
"total_tokens": 323,
413419
},
420+
tool_definitions=[FETCH_CONCEPT_TOOL_DEFINITION],
414421
tags={"service": "aws.bedrock-runtime", "ml_app": "<ml-app-name>"},
415422
)
416423

@@ -581,7 +588,9 @@ def test_llmobs_converse_tool_result_text(self, bedrock_client, request_vcr, moc
581588
)
582589

583590
assert len(llmobs_events) == 1
584-
assert llmobs_events[0]["meta"]["input"]["messages"] == [{"content": "bar", "role": "tool", "tool_id": "foo"}]
591+
assert llmobs_events[0]["meta"]["input"]["messages"] == [
592+
{"tool_results": [{"result": "bar", "tool_id": "foo", "type": "toolResult"}], "role": "user"}
593+
]
585594

586595
@pytest.mark.skipif(BOTO_VERSION < (1, 34, 131), reason="Converse API not available until botocore 1.34.131")
587596
def test_llmobs_converse_tool_result_json(self, bedrock_client, request_vcr, mock_tracer, llmobs_events):
@@ -601,7 +610,7 @@ def test_llmobs_converse_tool_result_json(self, bedrock_client, request_vcr, moc
601610

602611
assert len(llmobs_events) == 1
603612
assert llmobs_events[0]["meta"]["input"]["messages"] == [
604-
{"content": '{"result": "bar"}', "role": "tool", "tool_id": "foo"}
613+
{"tool_results": [{"result": '{"result": "bar"}', "tool_id": "foo", "type": "toolResult"}], "role": "user"}
605614
]
606615

607616
@pytest.mark.skipif(BOTO_VERSION < (1, 34, 131), reason="Converse API not available until botocore 1.34.131")
@@ -638,7 +647,12 @@ def test_llmobs_converse_tool_result_json_non_text_or_json(
638647

639648
assert len(llmobs_events) == 1
640649
assert llmobs_events[0]["meta"]["input"]["messages"] == [
641-
{"content": "[Unsupported content type(s): image]", "role": "tool", "tool_id": "foo"}
650+
{
651+
"tool_results": [
652+
{"result": "[Unsupported content type(s): image]", "tool_id": "foo", "type": "toolResult"}
653+
],
654+
"role": "user",
655+
}
642656
]
643657

644658

0 commit comments

Comments
 (0)