braintrust 0.3.9__tar.gz → 0.3.10__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.
Files changed (121) hide show
  1. {braintrust-0.3.9 → braintrust-0.3.10}/PKG-INFO +4 -1
  2. {braintrust-0.3.9 → braintrust-0.3.10}/setup.py +1 -0
  3. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/cli/install/api.py +0 -24
  4. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/cli/push.py +2 -1
  5. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/conftest.py +9 -0
  6. braintrust-0.3.10/src/braintrust/contrib/__init__.py +5 -0
  7. braintrust-0.3.10/src/braintrust/contrib/temporal/__init__.py +438 -0
  8. braintrust-0.3.10/src/braintrust/contrib/temporal/test_temporal.py +502 -0
  9. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/framework2.py +19 -0
  10. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/logger.py +8 -9
  11. braintrust-0.3.10/src/braintrust/test_framework2.py +233 -0
  12. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/test_logger.py +2 -0
  13. braintrust-0.3.10/src/braintrust/version.py +4 -0
  14. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust.egg-info/PKG-INFO +4 -1
  15. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust.egg-info/SOURCES.txt +4 -0
  16. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust.egg-info/requires.txt +8 -0
  17. braintrust-0.3.9/src/braintrust/version.py +0 -4
  18. {braintrust-0.3.9 → braintrust-0.3.10}/README.md +0 -0
  19. {braintrust-0.3.9 → braintrust-0.3.10}/setup.cfg +0 -0
  20. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/__init__.py +0 -0
  21. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/_generated_types.py +0 -0
  22. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/audit.py +0 -0
  23. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/aws.py +0 -0
  24. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/bt_json.py +0 -0
  25. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/cli/__init__.py +0 -0
  26. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/cli/__main__.py +0 -0
  27. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/cli/eval.py +0 -0
  28. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/cli/install/__init__.py +0 -0
  29. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/cli/install/bump_versions.py +0 -0
  30. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/cli/install/logs.py +0 -0
  31. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/cli/install/redshift.py +0 -0
  32. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/cli/install/run_migrations.py +0 -0
  33. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/context.py +0 -0
  34. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/db_fields.py +0 -0
  35. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/devserver/__init__.py +0 -0
  36. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/devserver/auth.py +0 -0
  37. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/devserver/cache.py +0 -0
  38. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/devserver/cors.py +0 -0
  39. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/devserver/dataset.py +0 -0
  40. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/devserver/eval_hooks.py +0 -0
  41. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/devserver/schemas.py +0 -0
  42. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/devserver/server.py +0 -0
  43. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/devserver/test_cached_login.py +0 -0
  44. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/devserver/test_lru_cache.py +0 -0
  45. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/devserver/test_server_integration.py +0 -0
  46. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/framework.py +0 -0
  47. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/functions/__init__.py +0 -0
  48. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/functions/constants.py +0 -0
  49. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/functions/invoke.py +0 -0
  50. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/functions/stream.py +0 -0
  51. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/generated_types.py +0 -0
  52. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/git_fields.py +0 -0
  53. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/gitutil.py +0 -0
  54. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/graph_util.py +0 -0
  55. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/http_headers.py +0 -0
  56. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/id_gen.py +0 -0
  57. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/merge_row_batch.py +0 -0
  58. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/oai.py +0 -0
  59. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/object.py +0 -0
  60. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/otel/__init__.py +0 -0
  61. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/otel/context.py +0 -0
  62. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/otel/test_distributed_tracing.py +0 -0
  63. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/otel/test_otel_bt_integration.py +0 -0
  64. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/parameters.py +0 -0
  65. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/prompt.py +0 -0
  66. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/prompt_cache/__init__.py +0 -0
  67. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/prompt_cache/disk_cache.py +0 -0
  68. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/prompt_cache/lru_cache.py +0 -0
  69. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/prompt_cache/prompt_cache.py +0 -0
  70. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/prompt_cache/test_disk_cache.py +0 -0
  71. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/prompt_cache/test_lru_cache.py +0 -0
  72. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/prompt_cache/test_prompt_cache.py +0 -0
  73. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/py.typed +0 -0
  74. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/queue.py +0 -0
  75. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/resource_manager.py +0 -0
  76. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/score.py +0 -0
  77. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/serializable_data_class.py +0 -0
  78. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/span_identifier_v1.py +0 -0
  79. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/span_identifier_v2.py +0 -0
  80. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/span_identifier_v3.py +0 -0
  81. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/span_identifier_v4.py +0 -0
  82. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/span_types.py +0 -0
  83. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/test_framework.py +0 -0
  84. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/test_helpers.py +0 -0
  85. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/test_id_gen.py +0 -0
  86. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/test_otel.py +0 -0
  87. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/test_queue.py +0 -0
  88. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/test_serializable_data_class.py +0 -0
  89. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/test_span_components.py +0 -0
  90. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/test_util.py +0 -0
  91. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/test_version.py +0 -0
  92. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/util.py +0 -0
  93. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/wrappers/__init__.py +0 -0
  94. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/wrappers/_anthropic_utils.py +0 -0
  95. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/wrappers/agno/__init__.py +0 -0
  96. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/wrappers/agno/agent.py +0 -0
  97. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/wrappers/agno/function_call.py +0 -0
  98. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/wrappers/agno/model.py +0 -0
  99. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/wrappers/agno/team.py +0 -0
  100. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/wrappers/agno/utils.py +0 -0
  101. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/wrappers/anthropic.py +0 -0
  102. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/wrappers/claude_agent_sdk/__init__.py +0 -0
  103. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/wrappers/claude_agent_sdk/_wrapper.py +0 -0
  104. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/wrappers/claude_agent_sdk/test_wrapper.py +0 -0
  105. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/wrappers/dspy.py +0 -0
  106. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/wrappers/google_genai/__init__.py +0 -0
  107. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/wrappers/langchain.py +0 -0
  108. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/wrappers/litellm.py +0 -0
  109. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/wrappers/openai.py +0 -0
  110. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/wrappers/test_agno.py +0 -0
  111. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/wrappers/test_anthropic.py +0 -0
  112. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/wrappers/test_dspy.py +0 -0
  113. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/wrappers/test_google_genai.py +0 -0
  114. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/wrappers/test_litellm.py +0 -0
  115. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/wrappers/test_openai.py +0 -0
  116. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/wrappers/test_pydantic_ai.py +0 -0
  117. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/wrappers/test_utils.py +0 -0
  118. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust/xact_ids.py +0 -0
  119. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust.egg-info/dependency_links.txt +0 -0
  120. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust.egg-info/entry_points.txt +0 -0
  121. {braintrust-0.3.9 → braintrust-0.3.10}/src/braintrust.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: braintrust
3
- Version: 0.3.9
3
+ Version: 0.3.10
4
4
  Summary: SDK for integrating Braintrust
5
5
  Home-page: https://www.braintrust.dev
6
6
  Author: Braintrust
@@ -35,6 +35,8 @@ Provides-Extra: otel
35
35
  Requires-Dist: opentelemetry-api; extra == "otel"
36
36
  Requires-Dist: opentelemetry-sdk; extra == "otel"
37
37
  Requires-Dist: opentelemetry-exporter-otlp-proto-http; extra == "otel"
38
+ Provides-Extra: temporal
39
+ Requires-Dist: temporalio>=1.19.0; python_version >= "3.10" and extra == "temporal"
38
40
  Provides-Extra: all
39
41
  Requires-Dist: boto3; extra == "all"
40
42
  Requires-Dist: openai-agents; extra == "all"
@@ -44,6 +46,7 @@ Requires-Dist: opentelemetry-sdk; extra == "all"
44
46
  Requires-Dist: psycopg2-binary; extra == "all"
45
47
  Requires-Dist: pydoc-markdown; extra == "all"
46
48
  Requires-Dist: starlette; extra == "all"
49
+ Requires-Dist: temporalio>=1.19.0; python_version >= "3.10" and extra == "all"
47
50
  Requires-Dist: uv; extra == "all"
48
51
  Requires-Dist: uvicorn; extra == "all"
49
52
  Dynamic: author
@@ -29,6 +29,7 @@ extras_require = {
29
29
  "doc": ["pydoc-markdown"],
30
30
  "openai-agents": ["openai-agents"],
31
31
  "otel": ["opentelemetry-api", "opentelemetry-sdk", "opentelemetry-exporter-otlp-proto-http"],
32
+ "temporal": ["temporalio>=1.19.0; python_version>='3.10'"],
32
33
  }
33
34
 
34
35
  extras_require["all"] = sorted({package for packages in extras_require.values() for package in packages})
@@ -4,7 +4,6 @@ import textwrap
4
4
  import time
5
5
 
6
6
  from botocore.exceptions import ClientError
7
-
8
7
  from braintrust.logger import app_conn, login
9
8
 
10
9
  # pylint: disable=no-name-in-module
@@ -30,8 +29,6 @@ PARAMS = {
30
29
  "PrivateSubnet2CIDR": "private_subnet_2_cidr",
31
30
  "PrivateSubnet3CIDR": "private_subnet_3_cidr",
32
31
  "ManagedPostgres": "managed_postgres",
33
- "ManagedClickhouse": "managed_clickhouse",
34
- "ClickhouseInstanceType": "clickhouse_instance_type",
35
32
  "PostgresVersion": "postgres_version",
36
33
  "OutboundRateLimitWindowMinutes": "outbound_rate_limit_window_minutes",
37
34
  "OutboundRateLimitMaxRequests": "outbound_rate_limit_max_requests",
@@ -179,19 +176,6 @@ def build_parser(subparsers, parents):
179
176
  default=None,
180
177
  )
181
178
 
182
- # Clickhouse
183
- parser.add_argument(
184
- "--managed-clickhouse",
185
- help="Spin up a Clickhouse Instance for faster analytics",
186
- default=None,
187
- choices=[None, "true", "false"],
188
- )
189
- parser.add_argument(
190
- "--clickhouse-instance-type",
191
- help="The instance type for the Clickhouse instance",
192
- default=None,
193
- )
194
-
195
179
  # ElastiCacheClusterId
196
180
  parser.add_argument("--elasticache-cluster-host", help="The ElastiCacheCluster host to use", default=None)
197
181
  parser.add_argument(
@@ -236,11 +220,6 @@ def build_parser(subparsers, parents):
236
220
  help="[Advanced] The postgres URL to use (if you are connecting to another VPC)",
237
221
  default=None,
238
222
  )
239
- parser.add_argument("--clickhouse-pg-url", help="[Advanced] The clickhouse PG URL to use", default=None)
240
- parser.add_argument("--clickhouse-connect-url", help="[Advanced] The clickhouse connect URL to use", default=None)
241
- parser.add_argument(
242
- "--clickhouse-catchup-etl-arn", help="[Advanced] The clickhouse catchup ETL ARN to use", default=None
243
- )
244
223
 
245
224
  # To configure your org
246
225
  parser.add_argument(
@@ -321,9 +300,6 @@ def main(args):
321
300
  PARAMS["ElastiCacheClusterHost"] = "elasticache_cluster_host"
322
301
  PARAMS["ElastiCacheClusterPort"] = "elasticache_cluster_port"
323
302
  PARAMS["PostgresUrl"] = "postgres_url"
324
- PARAMS["ClickhouseCatchupEtlArn"] = "clickhouse_catchup_etl_arn"
325
- PARAMS["ClickhouseConnectUrl"] = "clickhouse_connect_url"
326
- PARAMS["ClickhousePGUrl"] = "clickhouse_pg_url"
327
303
  PARAMS["EnableBrainstore"] = "enable_brainstore"
328
304
  PARAMS["BrainstoreInstanceKeyPairName"] = "brainstore_instance_key_pair_name"
329
305
  PARAMS["BrainstoreLicenseKey"] = "brainstore_license_key"
@@ -16,7 +16,6 @@ import zipfile
16
16
  from typing import Any, Dict, List, Optional
17
17
 
18
18
  import requests
19
-
20
19
  from braintrust.framework import _set_lazy_load
21
20
 
22
21
  from .. import api_conn, login, org_id, proxy_conn
@@ -250,6 +249,8 @@ def _collect_function_function_defs(
250
249
  },
251
250
  "if_exists": f.if_exists if f.if_exists else if_exists,
252
251
  }
252
+ if f.metadata is not None:
253
+ j["metadata"] = f.metadata
253
254
  if f.parameters is None:
254
255
  raise ValueError(f"Function {f.name} has no supplied parameters")
255
256
  j["function_schema"] = {
@@ -36,3 +36,12 @@ def override_app_url_for_tests():
36
36
  @pytest.fixture(autouse=True)
37
37
  def setup_braintrust():
38
38
  os.environ.setdefault("GOOGLE_API_KEY", os.getenv("GEMINI_API_KEY", "your_google_api_key_here"))
39
+
40
+
41
+ @pytest.fixture(autouse=True)
42
+ def reset_braintrust_state():
43
+ """Reset all Braintrust global state after each test."""
44
+ yield
45
+ from braintrust import logger
46
+
47
+ logger._state = logger.BraintrustState()
@@ -0,0 +1,5 @@
1
+ """Braintrust contrib integrations.
2
+
3
+ This module contains officially supported integrations with third-party frameworks.
4
+ These integrations are optional and require additional dependencies.
5
+ """
@@ -0,0 +1,438 @@
1
+ """Braintrust integration for Temporal workflows and activities.
2
+
3
+ This module provides Temporal integration that automatically traces workflow executions
4
+ and activities in Braintrust. To use this integration, install braintrust with the
5
+ temporal extra:
6
+
7
+ pip install braintrust[temporal]
8
+
9
+ Components
10
+ ----------
11
+
12
+ There are two main components:
13
+
14
+ - **BraintrustPlugin**: Use this for both Temporal clients and workers. It's a convenience
15
+ wrapper that automatically configures the interceptor and sandbox settings.
16
+
17
+ - **BraintrustInterceptor**: The underlying interceptor. You can use this directly if you
18
+ need more control, but ``BraintrustPlugin`` is recommended for most use cases.
19
+
20
+ Worker Setup
21
+ ------------
22
+
23
+ Use ``BraintrustPlugin`` when creating a worker::
24
+
25
+ import braintrust
26
+ from braintrust.contrib.temporal import BraintrustPlugin
27
+ from temporalio.client import Client
28
+ from temporalio.worker import Worker
29
+
30
+ braintrust.init_logger(project="my-project")
31
+
32
+ client = await Client.connect("localhost:7233")
33
+
34
+ worker = Worker(
35
+ client,
36
+ task_queue="my-queue",
37
+ workflows=[MyWorkflow],
38
+ activities=[my_activity],
39
+ plugins=[BraintrustPlugin()],
40
+ )
41
+
42
+ await worker.run()
43
+
44
+ Client Setup
45
+ ------------
46
+
47
+ Use ``BraintrustPlugin`` when creating a client to propagate span context to workflows::
48
+
49
+ import braintrust
50
+ from braintrust.contrib.temporal import BraintrustPlugin
51
+ from temporalio.client import Client
52
+
53
+ braintrust.init_logger(project="my-project")
54
+
55
+ client = await Client.connect(
56
+ "localhost:7233",
57
+ plugins=[BraintrustPlugin()],
58
+ )
59
+
60
+ # Spans created around workflow calls will be linked as parents
61
+ with braintrust.start_span(name="my-operation") as span:
62
+ result = await client.execute_workflow(
63
+ MyWorkflow.run,
64
+ args,
65
+ id="workflow-id",
66
+ task_queue="my-queue",
67
+ )
68
+
69
+ What Gets Traced
70
+ ----------------
71
+
72
+ The integration will automatically:
73
+
74
+ - Trace workflow executions
75
+ - Trace all activity executions
76
+ - Trace local activities
77
+ - Maintain parent-child relationships between client calls, workflows, and activities
78
+ - Handle child workflows
79
+ - Respect Temporal replay safety (no duplicate spans during replay)
80
+ """
81
+
82
+ import dataclasses
83
+ from typing import Any, Dict, Mapping, Optional, Type
84
+
85
+ import braintrust
86
+ import temporalio.activity
87
+ import temporalio.api.common.v1
88
+ import temporalio.client
89
+ import temporalio.converter
90
+ import temporalio.worker
91
+ import temporalio.workflow
92
+ from temporalio.plugin import SimplePlugin
93
+ from temporalio.worker import WorkflowRunner
94
+ from temporalio.worker.workflow_sandbox import SandboxedWorkflowRunner
95
+
96
+ # Braintrust dynamically chooses its context implementation at runtime based on
97
+ # BRAINTRUST_OTEL_COMPAT environment variable. When first accessed, it reads
98
+ # os.environ which is restricted in the sandbox. Therefore if the first use
99
+ # is inside the sandbox, it will fail. So we eagerly reference it here to
100
+ # force initialization at import time (before sandbox evaluation).
101
+ try:
102
+ braintrust.current_span()
103
+ except Exception:
104
+ # It's okay if this fails (e.g., no logger initialized yet)
105
+ pass
106
+
107
+ # Store module-level reference to braintrust.current_span to avoid re-importing
108
+ # inside extern functions (which can trigger sandbox restrictions)
109
+ _current_span = braintrust.current_span
110
+
111
+ # Header key for passing span context between client, workflows, and activities
112
+ _HEADER_KEY = "_braintrust-span"
113
+
114
+
115
+ class BraintrustInterceptor(temporalio.client.Interceptor, temporalio.worker.Interceptor):
116
+ """Braintrust interceptor for tracing Temporal workflows and activities.
117
+
118
+ This interceptor can be used with both Temporal clients and workers to automatically
119
+ trace workflow executions and activity runs. It maintains proper parent-child
120
+ relationships in the trace hierarchy and respects Temporal's replay safety requirements.
121
+
122
+ The interceptor:
123
+ - Creates spans for workflow executions (using sandbox_unrestricted)
124
+ - Captures activity execution as spans with metadata
125
+ - Propagates span context from client → workflow → activities
126
+ - Handles both regular activities and local activities
127
+ - Supports child workflows
128
+ - Logs errors from failed activities and workflows
129
+ - Ensures replay safety (no duplicate spans during workflow replay)
130
+ """
131
+
132
+ def __init__(self, logger: Optional[Any] = None) -> None:
133
+ """Initialize interceptor.
134
+
135
+ Args:
136
+ logger: Optional background logger for testing.
137
+ """
138
+ self.payload_converter = temporalio.converter.PayloadConverter.default
139
+ self._bg_logger = logger
140
+ # Capture logger instance at init time for cross-thread use
141
+ if logger:
142
+ braintrust.logger._state._override_bg_logger.logger = logger
143
+ self._logger = braintrust.current_logger()
144
+
145
+ def _get_logger(self) -> Optional[Any]:
146
+ """Get logger for creating spans.
147
+
148
+ Sets thread-local override if background logger provided (for testing),
149
+ then returns captured logger instance.
150
+ """
151
+ if self._bg_logger:
152
+ braintrust.logger._state._override_bg_logger.logger = self._bg_logger
153
+ return self._logger
154
+
155
+ def intercept_client(
156
+ self, next: temporalio.client.OutboundInterceptor
157
+ ) -> temporalio.client.OutboundInterceptor:
158
+ """Intercept client calls to propagate span context to workflows."""
159
+ return _BraintrustClientOutboundInterceptor(next, self)
160
+
161
+ def intercept_activity(
162
+ self, next: temporalio.worker.ActivityInboundInterceptor
163
+ ) -> temporalio.worker.ActivityInboundInterceptor:
164
+ """Intercept activity executions to create activity spans."""
165
+ return _BraintrustActivityInboundInterceptor(next, self)
166
+
167
+ def workflow_interceptor_class(
168
+ self, input: temporalio.worker.WorkflowInterceptorClassInput
169
+ ) -> Optional[Type["BraintrustWorkflowInboundInterceptor"]]:
170
+ """Return workflow interceptor class to propagate context to activities."""
171
+ input.unsafe_extern_functions["__braintrust_get_logger"] = self._get_logger
172
+ return BraintrustWorkflowInboundInterceptor
173
+
174
+ def _span_context_to_headers(
175
+ self,
176
+ span_context: Dict[str, Any],
177
+ headers: Mapping[str, temporalio.api.common.v1.Payload],
178
+ ) -> Mapping[str, temporalio.api.common.v1.Payload]:
179
+ """Add span context to headers."""
180
+ if span_context:
181
+ payloads = self.payload_converter.to_payloads([span_context])
182
+ if payloads:
183
+ headers = {
184
+ **headers,
185
+ _HEADER_KEY: payloads[0],
186
+ }
187
+ return headers
188
+
189
+ def _span_context_from_headers(
190
+ self, headers: Mapping[str, temporalio.api.common.v1.Payload]
191
+ ) -> Optional[Dict[str, Any]]:
192
+ """Extract span context from headers."""
193
+ if _HEADER_KEY not in headers:
194
+ return None
195
+ header_payload = headers.get(_HEADER_KEY)
196
+ if not header_payload:
197
+ return None
198
+ payloads = self.payload_converter.from_payloads([header_payload])
199
+ if not payloads:
200
+ return None
201
+ return payloads[0] if payloads[0] else None
202
+
203
+
204
+ class _BraintrustClientOutboundInterceptor(temporalio.client.OutboundInterceptor):
205
+ """Client interceptor that propagates span context to workflows."""
206
+
207
+ def __init__(
208
+ self, next: temporalio.client.OutboundInterceptor, root: BraintrustInterceptor
209
+ ) -> None:
210
+ super().__init__(next)
211
+ self.root = root
212
+
213
+ async def start_workflow(
214
+ self, input: temporalio.client.StartWorkflowInput
215
+ ) -> temporalio.client.WorkflowHandle[Any, Any]:
216
+ # Get current span context and add it to workflow headers
217
+ current_span = _current_span()
218
+ if current_span:
219
+ span_context = current_span.export()
220
+ input.headers = self.root._span_context_to_headers(span_context, input.headers)
221
+
222
+ return await super().start_workflow(input)
223
+
224
+
225
+ class _BraintrustActivityInboundInterceptor(temporalio.worker.ActivityInboundInterceptor):
226
+ """Activity interceptor that creates spans for activity executions."""
227
+
228
+ def __init__(
229
+ self,
230
+ next: temporalio.worker.ActivityInboundInterceptor,
231
+ root: BraintrustInterceptor,
232
+ ) -> None:
233
+ super().__init__(next)
234
+ self.root = root
235
+
236
+ async def execute_activity(
237
+ self, input: temporalio.worker.ExecuteActivityInput
238
+ ) -> Any:
239
+ info = temporalio.activity.info()
240
+
241
+ # Extract parent span context from headers
242
+ parent_span_context = self.root._span_context_from_headers(input.headers)
243
+
244
+ logger = self.root._get_logger()
245
+ if not logger:
246
+ return await super().execute_activity(input)
247
+
248
+ # Create Braintrust span for activity execution, linked to workflow span
249
+ span = logger.start_span(
250
+ name=f"temporal.activity.{info.activity_type}",
251
+ type="task",
252
+ parent=parent_span_context or None,
253
+ metadata={
254
+ "temporal.activity_type": info.activity_type,
255
+ "temporal.activity_id": info.activity_id,
256
+ "temporal.workflow_id": info.workflow_id,
257
+ "temporal.workflow_run_id": info.workflow_run_id,
258
+ },
259
+ )
260
+ span.set_current()
261
+
262
+ try:
263
+ result = await super().execute_activity(input)
264
+ return result
265
+ except Exception as e:
266
+ span.log(error=str(e))
267
+ raise
268
+ finally:
269
+ span.unset_current()
270
+ span.end()
271
+
272
+
273
+ class BraintrustWorkflowInboundInterceptor(temporalio.worker.WorkflowInboundInterceptor):
274
+ """Workflow interceptor that creates workflow spans and propagates context to activities.
275
+
276
+ This interceptor creates a span for the workflow execution using sandbox_unrestricted
277
+ to bypass Temporal's sandbox restrictions. The workflow span is the parent for all
278
+ activities and child workflows executed within it.
279
+ """
280
+
281
+ def __init__(self, next: temporalio.worker.WorkflowInboundInterceptor) -> None:
282
+ super().__init__(next)
283
+ self.payload_converter = temporalio.converter.PayloadConverter.default
284
+ self._parent_span_context: Optional[Dict[str, Any]] = None
285
+
286
+ def init(self, outbound: temporalio.worker.WorkflowOutboundInterceptor) -> None:
287
+ super().init(_BraintrustWorkflowOutboundInterceptor(outbound, self))
288
+
289
+ async def execute_workflow(
290
+ self, input: temporalio.worker.ExecuteWorkflowInput
291
+ ) -> Any:
292
+ # Extract parent span context from workflow headers (set by client)
293
+ parent_span_context = None
294
+ if _HEADER_KEY in input.headers:
295
+ header_payload = input.headers.get(_HEADER_KEY)
296
+ if header_payload:
297
+ payloads = self.payload_converter.from_payloads([header_payload])
298
+ if payloads:
299
+ parent_span_context = payloads[0]
300
+
301
+ # Store parent span context for activities (will be overwritten if we create a workflow span)
302
+ self._parent_span_context = parent_span_context
303
+
304
+ # Create a span for the workflow execution using sandbox_unrestricted
305
+ # to bypass the sandbox restrictions on logger state access
306
+ span = None
307
+ if not temporalio.workflow.unsafe.is_replaying():
308
+ with temporalio.workflow.unsafe.sandbox_unrestricted():
309
+ # Get logger via extern function (supports test logger parameter)
310
+ get_logger = temporalio.workflow.extern_functions()["__braintrust_get_logger"]
311
+ logger = get_logger()
312
+
313
+ if logger:
314
+ info = temporalio.workflow.info()
315
+ span = logger.start_span(
316
+ name=f"temporal.workflow.{info.workflow_type}",
317
+ type="task",
318
+ parent=parent_span_context or None,
319
+ metadata={
320
+ "temporal.workflow_type": info.workflow_type,
321
+ "temporal.workflow_id": info.workflow_id,
322
+ "temporal.run_id": info.run_id,
323
+ },
324
+ )
325
+ span.set_current()
326
+
327
+ # Update parent span context for activities
328
+ self._parent_span_context = span.export()
329
+
330
+ try:
331
+ result = await super().execute_workflow(input)
332
+ return result
333
+ except Exception as e:
334
+ if span:
335
+ with temporalio.workflow.unsafe.sandbox_unrestricted():
336
+ span.log(error=str(e))
337
+ raise
338
+ finally:
339
+ if span:
340
+ with temporalio.workflow.unsafe.sandbox_unrestricted():
341
+ span.unset_current()
342
+ span.end()
343
+
344
+
345
+ class _BraintrustWorkflowOutboundInterceptor(
346
+ temporalio.worker.WorkflowOutboundInterceptor
347
+ ):
348
+ """Outbound workflow interceptor that propagates span context to activities."""
349
+
350
+ def __init__(
351
+ self,
352
+ next: temporalio.worker.WorkflowOutboundInterceptor,
353
+ root: BraintrustWorkflowInboundInterceptor,
354
+ ) -> None:
355
+ super().__init__(next)
356
+ self.root = root
357
+
358
+ def _add_span_context_to_headers(
359
+ self, headers: Mapping[str, temporalio.api.common.v1.Payload]
360
+ ) -> Mapping[str, temporalio.api.common.v1.Payload]:
361
+ """Add parent span context to headers if available.
362
+
363
+ Note: We always pass span context through headers, even during replay,
364
+ so activities can maintain proper parent-child relationships. The replay
365
+ safety is handled in the activity interceptor, which only creates spans
366
+ when the activity actually executes (not during replay).
367
+ """
368
+ if self.root._parent_span_context:
369
+ payloads = self.root.payload_converter.to_payloads([self.root._parent_span_context])
370
+ if payloads:
371
+ return {**headers, _HEADER_KEY: payloads[0]}
372
+ return headers
373
+
374
+ def start_activity(
375
+ self, input: temporalio.worker.StartActivityInput
376
+ ) -> temporalio.workflow.ActivityHandle:
377
+ input.headers = self._add_span_context_to_headers(input.headers)
378
+ return super().start_activity(input)
379
+
380
+ def start_local_activity(
381
+ self, input: temporalio.worker.StartLocalActivityInput
382
+ ) -> temporalio.workflow.ActivityHandle:
383
+ input.headers = self._add_span_context_to_headers(input.headers)
384
+ return super().start_local_activity(input)
385
+
386
+ def start_child_workflow(
387
+ self, input: temporalio.worker.StartChildWorkflowInput
388
+ ) -> temporalio.workflow.ChildWorkflowHandle:
389
+ input.headers = self._add_span_context_to_headers(input.headers)
390
+ return super().start_child_workflow(input)
391
+
392
+
393
+ def _modify_workflow_runner(existing: Optional[WorkflowRunner]) -> Optional[WorkflowRunner]:
394
+ """Add braintrust to sandbox passthrough modules."""
395
+ if isinstance(existing, SandboxedWorkflowRunner):
396
+ new_restrictions = existing.restrictions.with_passthrough_modules("braintrust")
397
+ return dataclasses.replace(existing, restrictions=new_restrictions)
398
+ return existing
399
+
400
+
401
+ class BraintrustPlugin(SimplePlugin):
402
+ """Braintrust plugin for Temporal that automatically configures tracing.
403
+
404
+ This plugin simplifies Braintrust integration with Temporal by:
405
+ - Automatically adding BraintrustInterceptor to the worker
406
+ - Configuring the sandbox to allow braintrust imports without unsafe.imports_passed_through()
407
+
408
+ Example usage:
409
+ from braintrust.contrib.temporal import BraintrustPlugin
410
+ from temporalio.worker import Worker
411
+
412
+ worker = Worker(
413
+ client,
414
+ task_queue="my-queue",
415
+ workflows=[MyWorkflow],
416
+ activities=[my_activity],
417
+ plugins=[BraintrustPlugin()],
418
+ )
419
+
420
+ Requires temporalio >= 1.19.0.
421
+ """
422
+
423
+ def __init__(self, logger: Optional[Any] = None) -> None:
424
+ """Initialize the Braintrust plugin.
425
+
426
+ Args:
427
+ logger: Optional background logger for testing.
428
+ """
429
+ interceptor = BraintrustInterceptor(logger=logger)
430
+ super().__init__(
431
+ name="braintrust",
432
+ client_interceptors=[interceptor],
433
+ worker_interceptors=[interceptor],
434
+ workflow_runner=_modify_workflow_runner,
435
+ )
436
+
437
+
438
+ __all__ = ["BraintrustInterceptor", "BraintrustPlugin"]