turingpulse-sdk-bedrock 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,42 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Virtual environments
7
+ .venv/
8
+ venv/
9
+ ENV/
10
+
11
+ # Distribution / packaging
12
+ dist/
13
+ build/
14
+ *.egg-info/
15
+
16
+ # Database files
17
+ *.db
18
+ *.sqlite3
19
+
20
+ # Environment variables
21
+ .env
22
+ .env.local
23
+
24
+ # IDE
25
+ .idea/
26
+ .vscode/
27
+ *.swp
28
+ *.swo
29
+
30
+ # Testing
31
+ .pytest_cache/
32
+ .coverage
33
+ htmlcov/
34
+ .tox/
35
+
36
+ # Logs
37
+ *.log
38
+ logs/
39
+
40
+ # OS files
41
+ .DS_Store
42
+ Thumbs.db
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: turingpulse-sdk-bedrock
3
+ Version: 1.0.0
4
+ Summary: TuringPulse SDK integration for AWS Bedrock
5
+ License-Expression: Apache-2.0
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: boto3>=1.35.0
8
+ Requires-Dist: turingpulse-sdk>=1.0.0
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
11
+ Requires-Dist: pytest>=8.0; extra == 'dev'
@@ -0,0 +1,17 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "turingpulse-sdk-bedrock"
7
+ version = "1.0.0"
8
+ description = "TuringPulse SDK integration for AWS Bedrock"
9
+ requires-python = ">=3.11"
10
+ license = "Apache-2.0"
11
+ dependencies = [
12
+ "turingpulse-sdk>=1.0.0",
13
+ "boto3>=1.35.0",
14
+ ]
15
+
16
+ [project.optional-dependencies]
17
+ dev = ["pytest>=8.0", "pytest-asyncio>=0.23"]
@@ -0,0 +1,6 @@
1
+ """TuringPulse SDK integration for AWS Bedrock."""
2
+
3
+ from ._wrapper import patch_bedrock, unpatch_bedrock
4
+
5
+ __version__ = "0.1.0"
6
+ __all__ = ["patch_bedrock", "unpatch_bedrock"]
@@ -0,0 +1,139 @@
1
+ """AWS Bedrock monkey-patch instrumentation for TuringPulse."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import os
8
+ from contextvars import ContextVar
9
+ from typing import Any, Optional
10
+
11
+ from turingpulse_sdk import instrument, GovernanceDirective
12
+ from turingpulse_sdk.config import MAX_FIELD_SIZE
13
+ from turingpulse_sdk.context import current_context
14
+ from turingpulse_sdk.exceptions import ConfigurationError
15
+
16
+ logger = logging.getLogger("turingpulse.sdk.bedrock")
17
+
18
+ _INSTRUMENTING: ContextVar[bool] = ContextVar("_tp_bedrock_instrumenting", default=False)
19
+
20
+ _ORIGINAL_CONVERSE: Any = None
21
+
22
+
23
+ def patch_bedrock(
24
+ *,
25
+ name: str | None = None,
26
+ governance: Optional[GovernanceDirective] = None,
27
+ ) -> None:
28
+ """Monkey-patch ``botocore`` to intercept ``bedrock-runtime.converse`` calls."""
29
+ global _ORIGINAL_CONVERSE
30
+
31
+ effective_name = name or os.getenv("TP_WORKFLOW_NAME", "")
32
+ if not effective_name:
33
+ raise ConfigurationError(
34
+ "patch_bedrock() requires name= or TP_WORKFLOW_NAME to be set."
35
+ )
36
+
37
+ try:
38
+ import botocore.client
39
+ except ImportError as exc:
40
+ raise ImportError("boto3 package is required: pip install boto3") from exc
41
+
42
+ if _ORIGINAL_CONVERSE is not None:
43
+ logger.warning("Bedrock is already patched — skipping")
44
+ return
45
+
46
+ _ORIGINAL_CONVERSE = botocore.client.ClientCreator.create_client
47
+
48
+ original_create = _ORIGINAL_CONVERSE
49
+
50
+ def _patched_create_client(self_creator, *args: Any, **kwargs: Any) -> Any:
51
+ client = original_create(self_creator, *args, **kwargs)
52
+ service_name = args[0] if args else kwargs.get("service_name", "")
53
+ if service_name == "bedrock-runtime":
54
+ _wrap_converse(client, effective_name, governance)
55
+ return client
56
+
57
+ botocore.client.ClientCreator.create_client = _patched_create_client
58
+ logger.info("Bedrock patched for TuringPulse instrumentation")
59
+
60
+
61
+ def unpatch_bedrock() -> None:
62
+ """Restore original botocore client creator."""
63
+ global _ORIGINAL_CONVERSE
64
+ if _ORIGINAL_CONVERSE is None:
65
+ return
66
+ try:
67
+ import botocore.client
68
+ botocore.client.ClientCreator.create_client = _ORIGINAL_CONVERSE
69
+ except ImportError:
70
+ pass
71
+ _ORIGINAL_CONVERSE = None
72
+ logger.info("Bedrock unpatched — original methods restored")
73
+
74
+
75
+ def _wrap_converse(client: Any, name: str, governance: Optional[GovernanceDirective]) -> None:
76
+ """Wrap the converse method of a bedrock-runtime client."""
77
+ original_converse = client.converse
78
+
79
+ @instrument(name=name, governance=governance)
80
+ def _patched_converse(**kwargs: Any) -> Any:
81
+ if _INSTRUMENTING.get(False):
82
+ return original_converse(**kwargs)
83
+ token = _INSTRUMENTING.set(True)
84
+ try:
85
+ response = original_converse(**kwargs)
86
+ _record_bedrock_span(response, kwargs)
87
+ return response
88
+ finally:
89
+ _INSTRUMENTING.reset(token)
90
+
91
+ client.converse = _patched_converse
92
+
93
+
94
+ def _record_bedrock_span(response: Any, kwargs: dict) -> None:
95
+ ctx = current_context()
96
+ if not ctx:
97
+ return
98
+ ctx.framework = "bedrock"
99
+ ctx.node_type = "llm"
100
+
101
+ usage = response.get("usage", {}) if isinstance(response, dict) else {}
102
+ ctx.set_tokens(
103
+ usage.get("inputTokens", 0),
104
+ usage.get("outputTokens", 0),
105
+ )
106
+
107
+ model_id = kwargs.get("modelId", "unknown")
108
+ ctx.set_model(model_id, "bedrock")
109
+
110
+ messages = kwargs.get("messages", [])
111
+ if messages:
112
+ last_user = next(
113
+ (m for m in reversed(messages) if m.get("role") == "user"),
114
+ None,
115
+ )
116
+ if last_user:
117
+ content = last_user.get("content", [])
118
+ if isinstance(content, list):
119
+ texts = [c.get("text", "") for c in content if "text" in c]
120
+ ctx.set_prompt("\n".join(texts)[:MAX_FIELD_SIZE])
121
+ elif isinstance(content, str):
122
+ ctx.set_prompt(content[:MAX_FIELD_SIZE])
123
+
124
+ output = response.get("output", {}) if isinstance(response, dict) else {}
125
+ message = output.get("message", {})
126
+ content = message.get("content", [])
127
+ if content:
128
+ texts = [c.get("text", "") for c in content if "text" in c]
129
+ if texts:
130
+ ctx.set_io(output_data="\n".join(texts)[:MAX_FIELD_SIZE])
131
+
132
+ for c in content:
133
+ if "toolUse" in c:
134
+ tool = c["toolUse"]
135
+ ctx.add_tool_call(
136
+ tool_name=tool.get("name", "unknown"),
137
+ tool_args=tool.get("input", {}),
138
+ tool_id=tool.get("toolUseId"),
139
+ )