|
2 | 2 | # SPDX-License-Identifier: Apache-2.0 |
3 | 3 | # Modifications Copyright The OpenTelemetry Authors. Licensed under the Apache License 2.0 License. |
4 | 4 | import importlib |
| 5 | +import json |
| 6 | +from typing import Any, Dict, Sequence |
5 | 7 |
|
6 | 8 | from botocore.exceptions import ClientError |
7 | 9 |
|
|
32 | 34 | _determine_call_context, |
33 | 35 | _safe_invoke, |
34 | 36 | ) |
35 | | -from opentelemetry.instrumentation.botocore.extensions import _KNOWN_EXTENSIONS, _find_extension |
| 37 | +from opentelemetry.instrumentation.botocore.extensions import _KNOWN_EXTENSIONS, _find_extension, bedrock_utils |
36 | 38 | from opentelemetry.instrumentation.botocore.extensions.dynamodb import _DynamoDbExtension |
37 | 39 | from opentelemetry.instrumentation.botocore.extensions.lmbd import _LambdaExtension |
38 | 40 | from opentelemetry.instrumentation.botocore.extensions.sns import _SnsExtension |
@@ -245,6 +247,87 @@ def _apply_botocore_bedrock_patch() -> None: |
245 | 247 | _KNOWN_EXTENSIONS["bedrock"] = _lazy_load(".", "_BedrockExtension") |
246 | 248 | _KNOWN_EXTENSIONS["bedrock-agent"] = _lazy_load(".", "_BedrockAgentExtension") |
247 | 249 | _KNOWN_EXTENSIONS["bedrock-agent-runtime"] = _lazy_load(".", "_BedrockAgentRuntimeExtension") |
| 250 | + |
| 251 | + # TODO: The following code is to patch bedrock-runtime bugs that are fixed in |
| 252 | + # opentelemetry-instrumentation-botocore==0.56b0 in these PRs: |
| 253 | + # https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3548 |
| 254 | + # https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3544 |
| 255 | + # Remove this code once we've bumped opentelemetry-instrumentation-botocore dependency to 0.56b0 |
| 256 | + |
| 257 | + old_init = bedrock_utils.ConverseStreamWrapper.__init__ |
| 258 | + old_process_event = bedrock_utils.ConverseStreamWrapper._process_event |
| 259 | + |
| 260 | + def patched_init(self, *args, **kwargs): |
| 261 | + old_init(self, *args, **kwargs) |
| 262 | + self._tool_json_input_buf = "" |
| 263 | + |
| 264 | + def patched_process_event(self, event): |
| 265 | + if "contentBlockStart" in event: |
| 266 | + start = event["contentBlockStart"].get("start", {}) |
| 267 | + if "toolUse" in start: |
| 268 | + self._content_block = {"toolUse": start["toolUse"]} |
| 269 | + return |
| 270 | + |
| 271 | + if "contentBlockDelta" in event: |
| 272 | + if self._record_message: |
| 273 | + delta = event["contentBlockDelta"].get("delta", {}) |
| 274 | + if "text" in delta: |
| 275 | + self._content_block.setdefault("text", "") |
| 276 | + self._content_block["text"] += delta["text"] |
| 277 | + elif "toolUse" in delta: |
| 278 | + if (input_buf := delta["toolUse"].get("input")) is not None: |
| 279 | + self._tool_json_input_buf += input_buf |
| 280 | + return |
| 281 | + |
| 282 | + if "contentBlockStop" in event: |
| 283 | + if self._record_message: |
| 284 | + if self._tool_json_input_buf: |
| 285 | + try: |
| 286 | + self._content_block["toolUse"]["input"] = json.loads(self._tool_json_input_buf) |
| 287 | + except json.JSONDecodeError: |
| 288 | + self._content_block["toolUse"]["input"] = self._tool_json_input_buf |
| 289 | + self._message["content"].append(self._content_block) |
| 290 | + self._content_block = {} |
| 291 | + self._tool_json_input_buf = "" |
| 292 | + return |
| 293 | + |
| 294 | + old_process_event(self, event) |
| 295 | + |
| 296 | + def patched_extract_tool_calls(message: dict[str, Any], capture_content: bool) -> Sequence[Dict[str, Any]] | None: |
| 297 | + content = message.get("content") |
| 298 | + if not content: |
| 299 | + return None |
| 300 | + |
| 301 | + tool_uses = [item["toolUse"] for item in content if "toolUse" in item] |
| 302 | + if not tool_uses: |
| 303 | + tool_uses = [item for item in content if isinstance(item, dict) and item.get("type") == "tool_use"] |
| 304 | + tool_id_key = "id" |
| 305 | + else: |
| 306 | + tool_id_key = "toolUseId" |
| 307 | + |
| 308 | + if not tool_uses: |
| 309 | + return None |
| 310 | + |
| 311 | + tool_calls = [] |
| 312 | + for tool_use in tool_uses: |
| 313 | + tool_call = {"type": "function"} |
| 314 | + if call_id := tool_use.get(tool_id_key): |
| 315 | + tool_call["id"] = call_id |
| 316 | + |
| 317 | + if function_name := tool_use.get("name"): |
| 318 | + tool_call["function"] = {"name": function_name} |
| 319 | + |
| 320 | + if (function_input := tool_use.get("input")) and capture_content: |
| 321 | + tool_call.setdefault("function", {}) |
| 322 | + tool_call["function"]["arguments"] = function_input |
| 323 | + |
| 324 | + tool_calls.append(tool_call) |
| 325 | + return tool_calls |
| 326 | + |
| 327 | + bedrock_utils.ConverseStreamWrapper.__init__ = patched_init |
| 328 | + bedrock_utils.ConverseStreamWrapper._process_event = patched_process_event |
| 329 | + bedrock_utils.extract_tool_calls = patched_extract_tool_calls |
| 330 | + |
248 | 331 | # bedrock-runtime is handled by upstream |
249 | 332 |
|
250 | 333 |
|
|
0 commit comments