braintrust 0.5.2__py3-none-any.whl → 0.5.3__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.
- braintrust/__init__.py +11 -0
- braintrust/_generated_types.py +56 -3
- braintrust/db_fields.py +10 -0
- braintrust/generated_types.py +3 -1
- braintrust/logger.py +324 -125
- braintrust/merge_row_batch.py +49 -109
- braintrust/test_context.py +1264 -0
- braintrust/test_merge_row_batch.py +160 -0
- braintrust/version.py +2 -2
- braintrust/wrappers/threads.py +114 -0
- {braintrust-0.5.2.dist-info → braintrust-0.5.3.dist-info}/METADATA +1 -1
- {braintrust-0.5.2.dist-info → braintrust-0.5.3.dist-info}/RECORD +15 -13
- braintrust/graph_util.py +0 -147
- {braintrust-0.5.2.dist-info → braintrust-0.5.3.dist-info}/WHEEL +0 -0
- {braintrust-0.5.2.dist-info → braintrust-0.5.3.dist-info}/entry_points.txt +0 -0
- {braintrust-0.5.2.dist-info → braintrust-0.5.3.dist-info}/top_level.txt +0 -0
|
@@ -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
|
@@ -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,22 +1,21 @@
|
|
|
1
|
-
braintrust/__init__.py,sha256=
|
|
2
|
-
braintrust/_generated_types.py,sha256=
|
|
1
|
+
braintrust/__init__.py,sha256=DU4gzkV0R3nsWxp2da7iQS1MM_T9xHkrKSQE6nAnZbE,2627
|
|
2
|
+
braintrust/_generated_types.py,sha256=1gNbTwsy7LEBdLW-uYqzQ2v78ToAFX0PSmeE_NOJC18,101153
|
|
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=
|
|
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=
|
|
12
|
+
braintrust/generated_types.py,sha256=MUpi5l9XIW0bGqnYqa9DE7XbxDYsBI82ANyTVHz9DVg,5047
|
|
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=
|
|
19
|
-
braintrust/merge_row_batch.py,sha256=
|
|
17
|
+
braintrust/logger.py,sha256=ofx0_8ywVk_iyyqOH-6kCgRaYmm_sO7guVJ5hW2ISNg,217467
|
|
18
|
+
braintrust/merge_row_batch.py,sha256=mCutumLDOpH8ArP_4K5swP93mve7gmgfAnQdkdjUZ5E,6271
|
|
20
19
|
braintrust/oai.py,sha256=K7NLw7-3U7TJCyBVeoOwpy_UOmGlpaXXJcHZgW-rzHM,37746
|
|
21
20
|
braintrust/object.py,sha256=vYLyYWncsqLD00zffZUJwGTSkcJF9IIXmgIzrx3Np5c,632
|
|
22
21
|
braintrust/parameters.py,sha256=sQWfw18QXdPSnMHsF7aRrPmP7Zx6HEz9vaTUXWreudg,5911
|
|
@@ -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=
|
|
54
|
+
braintrust/version.py,sha256=u7mu2r_lw0F22zAYs8OHbW11HrkdIhoAK-RbDs2uQdA,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
|
|
@@ -113,6 +114,7 @@ braintrust/wrappers/test_openrouter.py,sha256=8HUfILPugOMqcvttpq76KQrynFb0xZpazv
|
|
|
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.
|
|
127
|
-
braintrust-0.5.
|
|
128
|
-
braintrust-0.5.
|
|
129
|
-
braintrust-0.5.
|
|
130
|
-
braintrust-0.5.
|
|
128
|
+
braintrust-0.5.3.dist-info/METADATA,sha256=snLiF8lw3wJkTkmgZnbthtfApgwnFb7eKFNARCWK0mM,3753
|
|
129
|
+
braintrust-0.5.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
130
|
+
braintrust-0.5.3.dist-info/entry_points.txt,sha256=Zpc0_09g5xm8as5jHqqFq7fhwO0xHSNct_TrEMONS7Q,60
|
|
131
|
+
braintrust-0.5.3.dist-info/top_level.txt,sha256=hw1-y-UFMf60RzAr8x_eM7SThbIuWfQsQIbVvqSF83A,11
|
|
132
|
+
braintrust-0.5.3.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|