braintrust 0.5.2__py3-none-any.whl → 0.5.4__py3-none-any.whl

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,160 @@
1
+ import unittest
2
+
3
+ from braintrust.db_fields import IS_MERGE_FIELD
4
+ from braintrust.merge_row_batch import batch_items, merge_row_batch
5
+
6
+
7
+ class MergeRowBatchTest(unittest.TestCase):
8
+ def test_basic(self):
9
+ rows = [
10
+ # These rows should get merged together, ending up as a merge.
11
+ dict(
12
+ experiment_id="e0",
13
+ id="x",
14
+ inputs=dict(a=12),
15
+ **{IS_MERGE_FIELD: True},
16
+ ),
17
+ dict(
18
+ experiment_id="e0",
19
+ id="x",
20
+ inputs=dict(b=10),
21
+ **{IS_MERGE_FIELD: True},
22
+ ),
23
+ dict(
24
+ experiment_id="e0",
25
+ id="x",
26
+ inputs=dict(c="hello"),
27
+ **{IS_MERGE_FIELD: True},
28
+ ),
29
+ # The first row should be clobbered by the second, but the third
30
+ # merged with the second, ending up as a replacement.
31
+ dict(
32
+ experiment_id="e0",
33
+ id="y",
34
+ inputs=dict(a="hello"),
35
+ ),
36
+ dict(
37
+ experiment_id="e0",
38
+ id="y",
39
+ inputs=dict(b=10),
40
+ ),
41
+ dict(
42
+ experiment_id="e0",
43
+ id="y",
44
+ inputs=dict(c=12),
45
+ **{IS_MERGE_FIELD: True},
46
+ ),
47
+ # These rows should be clobbered separately from the last batch.
48
+ dict(
49
+ dataset_id="d0",
50
+ id="y",
51
+ inputs=dict(a="hello"),
52
+ ),
53
+ dict(
54
+ dataset_id="d0",
55
+ id="y",
56
+ inputs=dict(b=10),
57
+ ),
58
+ dict(
59
+ dataset_id="d0",
60
+ id="y",
61
+ inputs=dict(c=12),
62
+ ),
63
+ ]
64
+
65
+ merged_rows = merge_row_batch(rows)
66
+ key_to_rows = {(row.get("experiment_id"), row.get("dataset_id"), row.get("id")): row for row in merged_rows}
67
+ self.assertEqual(
68
+ {
69
+ ("e0", None, "x"): dict(
70
+ experiment_id="e0",
71
+ id="x",
72
+ inputs=dict(a=12, b=10, c="hello"),
73
+ **{IS_MERGE_FIELD: True},
74
+ ),
75
+ ("e0", None, "y"): dict(
76
+ experiment_id="e0",
77
+ id="y",
78
+ inputs=dict(b=10, c=12),
79
+ ),
80
+ (None, "d0", "y"): dict(
81
+ dataset_id="d0",
82
+ id="y",
83
+ inputs=dict(c=12),
84
+ ),
85
+ },
86
+ key_to_rows,
87
+ )
88
+
89
+ def test_skip_fields(self):
90
+ rows = [
91
+ # These rows should get merged together, ending up as a merge. But
92
+ # the original fields should be retained, regardless of whether we
93
+ # populated them or not.
94
+ dict(
95
+ experiment_id="e0",
96
+ id="x",
97
+ inputs=dict(a=12),
98
+ **{IS_MERGE_FIELD: True},
99
+ created=123,
100
+ root_span_id="abc",
101
+ _parent_id="baz",
102
+ span_parents=["foo", "bar"],
103
+ ),
104
+ dict(
105
+ experiment_id="e0",
106
+ id="x",
107
+ inputs=dict(b=10),
108
+ **{IS_MERGE_FIELD: True},
109
+ created=456,
110
+ span_id="foo",
111
+ root_span_id="bar",
112
+ _parent_id="boop",
113
+ span_parents=[],
114
+ ),
115
+ ]
116
+
117
+ merged_rows = merge_row_batch(rows)
118
+ self.assertEqual(
119
+ merged_rows,
120
+ [
121
+ dict(
122
+ experiment_id="e0",
123
+ id="x",
124
+ inputs=dict(a=12, b=10),
125
+ **{IS_MERGE_FIELD: True},
126
+ created=123,
127
+ root_span_id="abc",
128
+ _parent_id="baz",
129
+ span_parents=["foo", "bar"],
130
+ ),
131
+ ],
132
+ )
133
+
134
+
135
+ class BatchItemsTest(unittest.TestCase):
136
+ def test_basic(self):
137
+ a = "x" * 1
138
+ b = "x" * 2
139
+ c = "x" * 4
140
+ d = "y" * 1
141
+ e = "y" * 2
142
+ f = "y" * 4
143
+
144
+ items = [a, b, c, f, e, d]
145
+
146
+ # No limits.
147
+ output = batch_items(items)
148
+ self.assertEqual(output, [[a, b, c, f, e, d]])
149
+
150
+ # Num items limit.
151
+ output = batch_items(items, batch_max_num_items=2)
152
+ self.assertEqual(output, [[a, b], [c, f], [e, d]])
153
+
154
+ # Num bytes limit.
155
+ output = batch_items(items, batch_max_num_bytes=2)
156
+ self.assertEqual(output, [[a], [b], [c], [f], [e], [d]])
157
+
158
+ # Both items and num bytes limit.
159
+ output = batch_items(items, batch_max_num_items=2, batch_max_num_bytes=5)
160
+ self.assertEqual(output, [[a, b], [c], [f], [e, d]])
braintrust/version.py CHANGED
@@ -1,4 +1,4 @@
1
- VERSION = "0.5.2"
1
+ VERSION = "0.5.4"
2
2
 
3
3
  # this will be templated during the build
4
- GIT_COMMIT = "25868bc58450dad2058b6499ce3bb9400330fbd1"
4
+ GIT_COMMIT = "449f6eeddd54f73d88e185fcf499087d3e61fbe6"
@@ -5,6 +5,7 @@ import braintrust
5
5
  import openai
6
6
  import pytest
7
7
  from braintrust import logger, wrap_openai
8
+ from braintrust.oai import ChatCompletionWrapper
8
9
  from braintrust.test_helpers import assert_dict_matches, init_test_logger
9
10
  from braintrust.wrappers.test_utils import assert_metrics_are_valid, run_in_subprocess, verify_autoinstrument_script
10
11
  from openai import AsyncOpenAI
@@ -377,7 +378,6 @@ def test_openai_responses_sparse_indices(memory_logger):
377
378
  # No spans should be generated from this unit test
378
379
  assert not memory_logger.pop()
379
380
 
380
-
381
381
  @pytest.mark.vcr
382
382
  def test_openai_embeddings(memory_logger):
383
383
  assert not memory_logger.pop()
@@ -1933,3 +1933,102 @@ class TestAutoInstrumentOpenAI:
1933
1933
  def test_auto_instrument_openai(self):
1934
1934
  """Test auto_instrument patches OpenAI, creates spans, and uninstrument works."""
1935
1935
  verify_autoinstrument_script("test_auto_openai.py")
1936
+
1937
+ class TestZAICompatibleOpenAI:
1938
+ """Tests for validating some ZAI compatibility with OpenAI wrapper."""
1939
+
1940
+ def test_chat_completion_streaming_none_arguments(self, memory_logger):
1941
+ """Test that ChatCompletionWrapper handles None arguments in tool calls (e.g., GLM-4.6 behavior)."""
1942
+ assert not memory_logger.pop()
1943
+
1944
+ # Simulate streaming results with None arguments in tool calls
1945
+ # This mimics the behavior of GLM-4.6 which returns {'arguments': None, 'name': 'weather'}
1946
+ all_results = [
1947
+ # First chunk: initial tool call with None arguments
1948
+ {
1949
+ "choices": [
1950
+ {
1951
+ "delta": {
1952
+ "role": "assistant",
1953
+ "tool_calls": [
1954
+ {
1955
+ "id": "call_123",
1956
+ "type": "function",
1957
+ "function": {
1958
+ "name": "get_weather",
1959
+ "arguments": None, # GLM-4.6 returns None here
1960
+ },
1961
+ }
1962
+ ],
1963
+ },
1964
+ "finish_reason": None,
1965
+ }
1966
+ ],
1967
+ },
1968
+ # Second chunk: subsequent tool call arguments (also None)
1969
+ {
1970
+ "choices": [
1971
+ {
1972
+ "delta": {
1973
+ "tool_calls": [
1974
+ {
1975
+ "function": {
1976
+ "arguments": None, # Subsequent chunks can also have None
1977
+ }
1978
+ }
1979
+ ],
1980
+ },
1981
+ "finish_reason": None,
1982
+ }
1983
+ ],
1984
+ },
1985
+ # Third chunk: actual arguments
1986
+ {
1987
+ "choices": [
1988
+ {
1989
+ "delta": {
1990
+ "tool_calls": [
1991
+ {
1992
+ "function": {
1993
+ "arguments": '{"city": "New York"}',
1994
+ }
1995
+ }
1996
+ ],
1997
+ },
1998
+ "finish_reason": None,
1999
+ }
2000
+ ],
2001
+ },
2002
+ # Final chunk
2003
+ {
2004
+ "choices": [
2005
+ {
2006
+ "delta": {},
2007
+ "finish_reason": "tool_calls",
2008
+ }
2009
+ ],
2010
+ },
2011
+ ]
2012
+
2013
+ # Process the results
2014
+ wrapper = ChatCompletionWrapper(None, None)
2015
+ result = wrapper._postprocess_streaming_results(all_results)
2016
+
2017
+ # Verify the output was built correctly
2018
+ assert "output" in result
2019
+ assert len(result["output"]) == 1
2020
+ message = result["output"][0]["message"]
2021
+ assert message["role"] == "assistant"
2022
+ assert message["tool_calls"] is not None
2023
+ assert len(message["tool_calls"]) == 1
2024
+
2025
+ # Verify the tool call was assembled correctly despite None arguments
2026
+ tool_call = message["tool_calls"][0]
2027
+ assert tool_call["id"] == "call_123"
2028
+ assert tool_call["type"] == "function"
2029
+ assert tool_call["function"]["name"] == "get_weather"
2030
+ # The arguments should be the concatenation: "" + "" + '{"city": "New York"}'
2031
+ assert tool_call["function"]["arguments"] == '{"city": "New York"}'
2032
+
2033
+ # No spans should be generated from this unit test
2034
+ assert not memory_logger.pop()
@@ -0,0 +1,114 @@
1
+ import contextvars
2
+ import functools
3
+ import logging
4
+ import threading
5
+ from concurrent import futures
6
+ from typing import Any, TypeVar
7
+
8
+ from wrapt import wrap_function_wrapper # pyright: ignore[reportUnknownVariableType, reportMissingTypeStubs]
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ __all__ = ["setup_threads", "patch_thread", "patch_thread_pool_executor"]
13
+
14
+
15
+ def setup_threads() -> bool:
16
+ """
17
+ Setup automatic context propagation for threading.
18
+
19
+ This patches stdlib threading primitives to automatically
20
+ propagate Braintrust context across thread boundaries.
21
+
22
+ Enable via:
23
+ - BRAINTRUST_INSTRUMENT_THREADS=true env var (automatic)
24
+ - Call this function directly (manual)
25
+
26
+ Returns:
27
+ bool: True if instrumentation was successful, False otherwise.
28
+ """
29
+ try:
30
+ patch_thread(threading.Thread)
31
+ patch_thread_pool_executor(futures.ThreadPoolExecutor)
32
+
33
+ logger.debug("Braintrust thread instrumentation enabled")
34
+ return True
35
+
36
+ except Exception as e:
37
+ logger.warning(f"Failed to enable thread instrumentation: {e}")
38
+ return False
39
+
40
+
41
+ T = TypeVar("T", bound=type[threading.Thread])
42
+
43
+
44
+ def patch_thread(thread_cls: T) -> T:
45
+ if __is_patched(thread_cls):
46
+ return thread_cls
47
+
48
+ def _wrap_thread_start(wrapped: Any, instance: Any, args: Any, kwargs: Any) -> Any:
49
+ try:
50
+ instance._braintrust_context = contextvars.copy_context()
51
+ except Exception as e:
52
+ logger.debug(f"Failed to capture context in thread start: {e}")
53
+ return wrapped(*args, **kwargs)
54
+
55
+ wrap_function_wrapper(thread_cls, "start", _wrap_thread_start)
56
+
57
+ def _wrap_thread_run(wrapped: Any, instance: Any, args: Any, kwargs: Any) -> Any:
58
+ try:
59
+ if hasattr(instance, "_braintrust_context"):
60
+ return instance._braintrust_context.run(wrapped, *args, **kwargs)
61
+ except Exception as e:
62
+ logger.debug(f"Failed to restore context in thread run: {e}")
63
+ return wrapped(*args, **kwargs)
64
+
65
+ wrap_function_wrapper(thread_cls, "run", _wrap_thread_run)
66
+
67
+ __mark_patched(thread_cls)
68
+ return thread_cls
69
+
70
+
71
+ def __is_patched(obj: Any) -> bool:
72
+ """Check if an object has already been patched."""
73
+ return getattr(obj, "_braintrust_patched", False)
74
+
75
+
76
+ def __mark_patched(obj: Any) -> None:
77
+ setattr(obj, "_braintrust_patched", True)
78
+
79
+
80
+ P = TypeVar("P", bound=type[futures.ThreadPoolExecutor])
81
+
82
+
83
+ def patch_thread_pool_executor(executor_cls: P) -> P:
84
+ if __is_patched(executor_cls):
85
+ return executor_cls
86
+
87
+ def _wrap_executor_submit(wrapped: Any, instance: Any, args: Any, kwargs: Any) -> Any:
88
+ try:
89
+ if not args:
90
+ return wrapped(*args, **kwargs)
91
+
92
+ func = args[0]
93
+ ctx = contextvars.copy_context()
94
+
95
+ @functools.wraps(func)
96
+ def context_wrapper(*func_args: Any, **func_kwargs: Any) -> Any:
97
+ try:
98
+ return ctx.run(func, *func_args, **func_kwargs)
99
+ except Exception as e:
100
+ # context.run() can fail if token is invalid
101
+ logger.debug(f"Failed to run in captured context: {e}")
102
+ return func(*func_args, **func_kwargs)
103
+
104
+ new_args = (context_wrapper,) + args[1:]
105
+ return wrapped(*new_args, **kwargs)
106
+ except Exception as e:
107
+ # Wrapping can fail - fall back to original
108
+ logger.debug(f"Failed to wrap executor submit: {e}")
109
+ return wrapped(*args, **kwargs)
110
+
111
+ wrap_function_wrapper(executor_cls, "submit", _wrap_executor_submit)
112
+
113
+ __mark_patched(executor_cls)
114
+ return executor_cls
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: braintrust
3
- Version: 0.5.2
3
+ Version: 0.5.4
4
4
  Summary: SDK for integrating Braintrust
5
5
  Home-page: https://www.braintrust.dev
6
6
  Author: Braintrust
@@ -1,23 +1,22 @@
1
- braintrust/__init__.py,sha256=Bn-JzKyigMCQs9XqjtWQwLoUtConMO-icUCPeIKuYCk,2330
2
- braintrust/_generated_types.py,sha256=_eqozb1MG75khlsvXoAV7oKjbgEEBvnzlLtiUfpxX8U,99447
1
+ braintrust/__init__.py,sha256=DU4gzkV0R3nsWxp2da7iQS1MM_T9xHkrKSQE6nAnZbE,2627
2
+ braintrust/_generated_types.py,sha256=d1z6xRHPc0iqifiaoCh7YwDQNSxKZAhM2KYmG0Fyrvk,104819
3
3
  braintrust/audit.py,sha256=3GQKzuTcFquYdrJtABM-k3xMlOIqgVkfG6UyeQ8_028,461
4
4
  braintrust/auto.py,sha256=wf4Jb7hYoGS0Agpx-YjUYEW7wwwUpyAJDp-l3A-y6c0,4792
5
5
  braintrust/aws.py,sha256=OBz_SRyopgpCDSNvETLypzGwTXk-bNLn-Eisevnjfwo,377
6
6
  braintrust/bt_json.py,sha256=VNunedFUEfbvVEBuACHLRsjDr8lLD3_nFwB54MXtcbY,9385
7
7
  braintrust/conftest.py,sha256=qcS1T4tqXN5Ocaol6gEN9xGOXBqvTYPXXiBJjHnHC6Y,3140
8
8
  braintrust/context.py,sha256=bOo1Li29lUsi2DOOllIDar0oRuQNbkLNzJ7cYq5JTbo,4126
9
- braintrust/db_fields.py,sha256=vjVEyDxl2d13lvvTcVwT5paHvaT-1kgI-4Lg-gihRIw,476
9
+ braintrust/db_fields.py,sha256=AX-K5t7KqO-xHHOfVRv8bn1ww7gZd3RNIFfANkZ2W0U,709
10
10
  braintrust/framework.py,sha256=KoXFKCprfeEHq-AqjarENcmHHcTuvr4PScWtMbIQ6zg,62635
11
11
  braintrust/framework2.py,sha256=o0igz4vXbmn0jHJPhDYvx14rFnI3ntV8H6VJfyJYRtM,16542
12
- braintrust/generated_types.py,sha256=_IBZSkYTralTQM5hLhnbdIQS8_NhWr4V77kJPHtEUHY,4997
12
+ braintrust/generated_types.py,sha256=WExtraVBQZMS5DcCVSQl_k66p6VUY7cK7u1SSQhc_to,5127
13
13
  braintrust/git_fields.py,sha256=au5ayyuvt7y_ojE9LC98ypTZd3RgFdjhRc8eFxcjnto,1434
14
14
  braintrust/gitutil.py,sha256=RsW7cawJMgaAaTw6WeB1sShyfflkPb7yH2x7yuRv10c,5642
15
- braintrust/graph_util.py,sha256=Z2Uy8RaOq5iMe5mShhQqRDDIpXVitE-biVxDiFB-0Ds,5545
16
15
  braintrust/http_headers.py,sha256=9ZsDcsAKG04SGowsgchZktD6rG_oSTKWa8QyGUPA4xE,154
17
16
  braintrust/id_gen.py,sha256=4UWLWRhksf76IkYi4cKACSaQ3yNgausrMRlhiurhy74,1590
18
- braintrust/logger.py,sha256=lxR-4L6HK3PXa_ZQaVTd1-IEzxJ55NE1XmmPFLiwGFw,209535
19
- braintrust/merge_row_batch.py,sha256=tvuz3qdsa7HZBrzzAzoQqAXU7pKsHUebC7sw6RybbmA,9881
20
- braintrust/oai.py,sha256=K7NLw7-3U7TJCyBVeoOwpy_UOmGlpaXXJcHZgW-rzHM,37746
17
+ braintrust/logger.py,sha256=ofx0_8ywVk_iyyqOH-6kCgRaYmm_sO7guVJ5hW2ISNg,217467
18
+ braintrust/merge_row_batch.py,sha256=mCutumLDOpH8ArP_4K5swP93mve7gmgfAnQdkdjUZ5E,6271
19
+ braintrust/oai.py,sha256=ispeHAeykwF3VmGMnaVKZRn7Q80DMtpM7nitFrtybRQ,38193
21
20
  braintrust/object.py,sha256=vYLyYWncsqLD00zffZUJwGTSkcJF9IIXmgIzrx3Np5c,632
22
21
  braintrust/parameters.py,sha256=sQWfw18QXdPSnMHsF7aRrPmP7Zx6HEz9vaTUXWreudg,5911
23
22
  braintrust/prompt.py,sha256=pLzhXoBp7ebxNraZADflR4YQMW5Ycjmt5ucK80P4_h0,1875
@@ -33,12 +32,14 @@ braintrust/span_identifier_v3.py,sha256=RAvOK0lK0huH952kI5X1Q9TaAloD5to8jgTCuYMM
33
32
  braintrust/span_identifier_v4.py,sha256=uFT-OdzySo4uCeAaJC3VqH55Oy433xZGBdK2hiEsm2w,10044
34
33
  braintrust/span_types.py,sha256=cpTzCwUj4yBPbPLnzR23-VXIU2E9nl_dsVCSVMvtSkc,376
35
34
  braintrust/test_bt_json.py,sha256=pokqmFSQ3m8aB0XN1EEsS-zbv_N4KCYzad6VyOFtwPw,25188
35
+ braintrust/test_context.py,sha256=4wZOhwzGgAxt-CcN2izAxhgcY975oh_HUMkZXRwwTys,42754
36
36
  braintrust/test_framework.py,sha256=rPjJYfVzRSbjf8e0irVfCnE3q5Rl1O_2Dy2dPyC9-lo,18674
37
37
  braintrust/test_framework2.py,sha256=pSEEmBIyszAiYnpEVvDZgJqIe3lQ3T807LmIuBqV98w,7235
38
38
  braintrust/test_helpers.py,sha256=VSelzjkR2IHyy5QD6sYB_79VIXq-wEDslDwyx9H11kI,13528
39
39
  braintrust/test_http.py,sha256=RiW07psmoxO9ZB5ZUN1K1hIan7l4_F9YC2mW0iqSMr0,15751
40
40
  braintrust/test_id_gen.py,sha256=Oqp8Rxd1_9ptpEJSu_xi6STIquZpx9n7Kh-bGUKMQH0,2489
41
41
  braintrust/test_logger.py,sha256=zINMwUn2hT1_ElZRkVJP95CkTwtLhjUKpR5zIP7tn5Y,112653
42
+ braintrust/test_merge_row_batch.py,sha256=cQ_Hiq6wlWIs091Tqexgd3TXWPb0BJm1N1t7cCQ7ZM4,4892
42
43
  braintrust/test_otel.py,sha256=EyLOBMIZegvGyqqWl4vAlIazZfz1s4tWbgSAM68-2mM,31356
43
44
  braintrust/test_queue.py,sha256=872weDhTfxN1Vu4-cV8pZZ9XNE8UkX4WNcvc2mPtSDI,8431
44
45
  braintrust/test_score.py,sha256=PpfzNeaYhC4shMFqF85EczuMT0FCTr6l1rdEYWLe4g0,5857
@@ -50,7 +51,7 @@ braintrust/test_util.py,sha256=SuSKTmvNyaR9Rbgf2TYCUWxJpZHoA2BMx6n4nQfV_pM,8221
50
51
  braintrust/test_version.py,sha256=hk5JKjEFbNJ_ONc1VEkqHquflzre34RpFhCEYLTK8iA,1051
51
52
  braintrust/trace.py,sha256=PHxfaHApGP_MPMIndZw7atIa2iwKPETUoG-TbF2dv6A,13767
52
53
  braintrust/util.py,sha256=S-qMBNsT36r_3pJ4LNKZ-vvHRlJwy8Wy7M7NAdfNOug,8919
53
- braintrust/version.py,sha256=EQlkRKEKEIegwawQp7zpbqvdiSpYZwNnHduewzJEXVk,117
54
+ braintrust/version.py,sha256=9Ag0YVXSTg_ksT5pb04Gdog87NeKSiNXtmslh5ZQyhQ,117
54
55
  braintrust/xact_ids.py,sha256=bdyp88HjlyIkglgLSqYlCYscdSH6EWVyE14sR90Xl1s,658
55
56
  braintrust/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
57
  braintrust/cli/__main__.py,sha256=wCBKHGVmn3IT_yMXk5qfDwyI2SV2gf1tLr0NTxm9T8k,1519
@@ -108,11 +109,12 @@ braintrust/wrappers/test_google_genai.py,sha256=BKkPfLFk2JG1UNJ3mFKzv5OjioWqo7EQ
108
109
  braintrust/wrappers/test_langsmith_wrapper.py,sha256=wEbPNy4o7VVvcuHcsCJ-sy2EATvBxhUXTYFBQNkKCjs,10449
109
110
  braintrust/wrappers/test_litellm.py,sha256=MKukVH-C-Mwc0-cVJZhkLdXwiXupXHc8EiWYKq0X8V0,23620
110
111
  braintrust/wrappers/test_oai_attachments.py,sha256=_EtNXjQxPgqXmj6UYMZn9GF4GDZf8m_1_TrwiEk7HWQ,11100
111
- braintrust/wrappers/test_openai.py,sha256=pC-5zO2CbeG1wmQ5IvbCQ5vXic5sZgTCTW7347QwbwI,69474
112
+ braintrust/wrappers/test_openai.py,sha256=dBphKJT2-xSnK4mJ_xbKLFPBfpHzK07TmHxPTQ8ZQDg,73355
112
113
  braintrust/wrappers/test_openrouter.py,sha256=8HUfILPugOMqcvttpq76KQrynFb0xZpazvta7TTSF6A,3849
113
114
  braintrust/wrappers/test_pydantic_ai_integration.py,sha256=xUSkiY5HUI-Z9G_etjyX7rGlOjBEdEY9CwVHyWM3xEE,104460
114
115
  braintrust/wrappers/test_pydantic_ai_wrap_openai.py,sha256=OO5NrbothkMr4v2sZ-EZLH7-yLj3k6TfdLG4uzXAsQk,5090
115
116
  braintrust/wrappers/test_utils.py,sha256=wpLNRSCAZ42eG4srJrGFMAlfBSkhaCOcn2QwJXkRQ7s,3005
117
+ braintrust/wrappers/threads.py,sha256=rGQO6aFmFstrXA4xPEZCGTZJ1FC7CAS_4zF6ikszprc,3682
116
118
  braintrust/wrappers/agno/__init__.py,sha256=soVMMmm_nmMcX9rWxfmKw9ur03XNKRcDmlf3G4GbR4g,2306
117
119
  braintrust/wrappers/agno/agent.py,sha256=m3HCxQNotJviJswGYMxxsOnMilF-DNGqeFZFJa2Zhs4,6473
118
120
  braintrust/wrappers/agno/function_call.py,sha256=MSsHLGoBk98xGlm-CYnjE5pYBi-IJfokLhjtbYgJpX0,2206
@@ -123,8 +125,8 @@ braintrust/wrappers/claude_agent_sdk/__init__.py,sha256=4FUE59ii39jVfhMAfkOcU-TJ
123
125
  braintrust/wrappers/claude_agent_sdk/_wrapper.py,sha256=UNZBcgTu7X71zvJKy6e-QQFuz9j8kRS6Kd1VOh3OOXA,17755
124
126
  braintrust/wrappers/claude_agent_sdk/test_wrapper.py,sha256=RmSzTfDC3tkepEbc9S_KYFITsjGVyllYE1fF9roSqBk,10779
125
127
  braintrust/wrappers/google_genai/__init__.py,sha256=C7MzKUr17CEcIbP0kg9L9IjN-6suE3NQ-oSYAjXdR_g,15832
126
- braintrust-0.5.2.dist-info/METADATA,sha256=lFY0wRN89qL-A6hoaipBAFXXs_m6dtspmP3ElXqvcAQ,3753
127
- braintrust-0.5.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
128
- braintrust-0.5.2.dist-info/entry_points.txt,sha256=Zpc0_09g5xm8as5jHqqFq7fhwO0xHSNct_TrEMONS7Q,60
129
- braintrust-0.5.2.dist-info/top_level.txt,sha256=hw1-y-UFMf60RzAr8x_eM7SThbIuWfQsQIbVvqSF83A,11
130
- braintrust-0.5.2.dist-info/RECORD,,
128
+ braintrust-0.5.4.dist-info/METADATA,sha256=Z3DmXXIkkIpZ5XQ7I6ECeVg5G3SW29onbNWyoLlvOZM,3753
129
+ braintrust-0.5.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
130
+ braintrust-0.5.4.dist-info/entry_points.txt,sha256=Zpc0_09g5xm8as5jHqqFq7fhwO0xHSNct_TrEMONS7Q,60
131
+ braintrust-0.5.4.dist-info/top_level.txt,sha256=hw1-y-UFMf60RzAr8x_eM7SThbIuWfQsQIbVvqSF83A,11
132
+ braintrust-0.5.4.dist-info/RECORD,,
braintrust/graph_util.py DELETED
@@ -1,147 +0,0 @@
1
- # Generic graph algorithms.
2
-
3
- import dataclasses
4
- from typing import Protocol
5
-
6
-
7
- # An UndirectedGraph consists of a set of vertex labels and a set of edges
8
- # between vertices.
9
- @dataclasses.dataclass
10
- class UndirectedGraph:
11
- vertices: set[int]
12
- edges: set[tuple[int, int]]
13
-
14
-
15
- # An AdjacencyListGraph is a mapping from vertex label to the list of vertices
16
- # where there is a directed edge from the key to the value.
17
- AdjacencyListGraph = dict[int, set[int]]
18
-
19
-
20
- class FirstVisitF(Protocol):
21
- def __call__(self, vertex: int, *, parent_vertex: int | None, **kwargs) -> None:
22
- """Extras:
23
- - parent_vertex: the vertex which spawned the current vertex as its
24
- child during the depth-first search. `parent_vertex` is guaranteed
25
- to have been visited before the current one.
26
- """
27
- ...
28
-
29
-
30
- class LastVisitF(Protocol):
31
- def __call__(self, vertex: int) -> None: ...
32
-
33
-
34
- def depth_first_search(
35
- graph: AdjacencyListGraph,
36
- first_visit_f: FirstVisitF | None = None,
37
- last_visit_f: LastVisitF | None = None,
38
- visitation_order: list[int] | None = None,
39
- ) -> None:
40
- """A general depth-first search algorithm over a directed graph. As it
41
- traverses the graph, it invokes user-provided hooks when a vertex is *first*
42
- visited (before visiting its children) and when it is *last* visited (after
43
- visiting all its children).
44
-
45
- The first_visit_f and last_visit_f functions may be passed additional
46
- information beyond the vertex being visited as kwargs. See their type
47
- signatures for more details. For future proofing, you will likely want to
48
- capture **kwargs as a catchall in your functions.
49
-
50
- An optional `visitation_order` can be specified, which controls the order in
51
- which vertices will be first visited (outside of visiting them through a
52
- different vertex). It can also be used to limit the set of starting vertices
53
- considered. Otherwise, the DFS will visit all vertices in an unspecfied
54
- order.
55
- """
56
-
57
- # Check the validity of the graph.
58
- for vs in graph.values():
59
- for v in vs:
60
- assert v in graph
61
-
62
- first_visited_vertices = set()
63
- visitation_order = visitation_order if visitation_order is not None else graph.keys()
64
- events = list(reversed([("first", x, dict(parent_vertex=None)) for x in visitation_order]))
65
- while events:
66
- event_type, vertex, extras = events.pop()
67
-
68
- if event_type == "last":
69
- if last_visit_f:
70
- last_visit_f(vertex)
71
- continue
72
-
73
- # First visit of a node. If we've already visited it, skip.
74
- if vertex in first_visited_vertices:
75
- continue
76
- first_visited_vertices.add(vertex)
77
- if first_visit_f:
78
- first_visit_f(vertex, parent_vertex=extras["parent_vertex"])
79
-
80
- # Add 'first' visitation events for all the children of the vertex to
81
- # the stack. But before this, add a 'last' visitation event for this
82
- # vertex, so that once we've completed all the children, we get the last
83
- # visitation event for this one.
84
- events.append(("last", vertex, dict()))
85
- for child in graph[vertex]:
86
- events.append(("first", child, dict(parent_vertex=vertex)))
87
-
88
-
89
- def undirected_connected_components(graph: UndirectedGraph) -> list[list[int]]:
90
- """Group together all the connected components of an undirected graph.
91
- Return each group as a list of vertices.
92
- """
93
-
94
- # Perhaps the most performant way to implement this is via union find. But
95
- # in lieu of that, we can use a depth-first search over a direct-ified
96
- # version of the graph. Upon the first visit of each vertex, we assign it a
97
- # label equal to the label of the parent vertex. If there is no parent
98
- # vertex, we assign a new label. At the end, we can group together all the
99
- # vertices with the same label.
100
-
101
- directed_graph = {v: set() for v in graph.vertices}
102
- for i, j in graph.edges:
103
- directed_graph[i].add(j)
104
- directed_graph[j].add(i)
105
-
106
- label_counter = 0
107
- vertex_labels = {}
108
-
109
- def first_visit_f(vertex, parent_vertex, **kwargs):
110
- if parent_vertex is not None:
111
- label = vertex_labels[parent_vertex]
112
- else:
113
- nonlocal label_counter
114
- # pylint: disable=used-before-assignment
115
- label = label_counter
116
- label_counter += 1
117
- vertex_labels[vertex] = label
118
-
119
- depth_first_search(directed_graph, first_visit_f=first_visit_f)
120
- output = [[] for _ in range(label_counter)]
121
- for vertex, label in vertex_labels.items():
122
- output[label].append(vertex)
123
-
124
- return output
125
-
126
-
127
- def topological_sort(graph: AdjacencyListGraph, visitation_order: list[int] | None = None) -> list[int]:
128
- """The topological_sort function accepts a graph as input, with edges from
129
- parents to children. It returns an ordering where parents are guaranteed to
130
- come before their children.
131
-
132
- The `visitation_order` is forwarded directly to `depth_first_search`.
133
-
134
- Ordering with respect to cycles is unspecified. It is the caller's
135
- responsibility to check for cycles if it matters.
136
- """
137
-
138
- # We use DFS, where upon the 'last' visitation of a node, we append it to
139
- # the final ordering. Then we reverse the list at the end.
140
- reverse_ordering = []
141
-
142
- def last_visit_f(vertex, **kwargs):
143
- reverse_ordering.append(vertex)
144
-
145
- depth_first_search(graph, last_visit_f=last_visit_f, visitation_order=visitation_order)
146
- reverse_ordering.reverse()
147
- return reverse_ordering