braintrust 0.3.14__py3-none-any.whl → 0.4.0__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 +4 -0
- braintrust/_generated_types.py +1200 -611
- braintrust/audit.py +2 -2
- braintrust/cli/eval.py +6 -7
- braintrust/cli/push.py +11 -11
- braintrust/conftest.py +1 -0
- braintrust/context.py +12 -17
- braintrust/contrib/temporal/__init__.py +16 -27
- braintrust/contrib/temporal/test_temporal.py +8 -3
- braintrust/devserver/auth.py +8 -8
- braintrust/devserver/cache.py +3 -4
- braintrust/devserver/cors.py +8 -7
- braintrust/devserver/dataset.py +3 -5
- braintrust/devserver/eval_hooks.py +7 -6
- braintrust/devserver/schemas.py +22 -19
- braintrust/devserver/server.py +19 -12
- braintrust/devserver/test_cached_login.py +4 -4
- braintrust/framework.py +128 -140
- braintrust/framework2.py +88 -87
- braintrust/functions/invoke.py +93 -53
- braintrust/functions/stream.py +3 -2
- braintrust/generated_types.py +17 -1
- braintrust/git_fields.py +11 -11
- braintrust/gitutil.py +2 -3
- braintrust/graph_util.py +10 -10
- braintrust/id_gen.py +2 -2
- braintrust/logger.py +346 -357
- braintrust/merge_row_batch.py +10 -9
- braintrust/oai.py +107 -24
- braintrust/otel/__init__.py +49 -49
- braintrust/otel/context.py +16 -30
- braintrust/otel/test_distributed_tracing.py +14 -11
- braintrust/otel/test_otel_bt_integration.py +32 -31
- braintrust/parameters.py +8 -8
- braintrust/prompt.py +14 -14
- braintrust/prompt_cache/disk_cache.py +5 -4
- braintrust/prompt_cache/lru_cache.py +3 -2
- braintrust/prompt_cache/prompt_cache.py +13 -14
- braintrust/queue.py +4 -4
- braintrust/score.py +4 -4
- braintrust/serializable_data_class.py +4 -4
- braintrust/span_identifier_v1.py +1 -2
- braintrust/span_identifier_v2.py +3 -4
- braintrust/span_identifier_v3.py +23 -20
- braintrust/span_identifier_v4.py +34 -25
- braintrust/test_framework.py +16 -6
- braintrust/test_helpers.py +5 -5
- braintrust/test_id_gen.py +2 -3
- braintrust/test_otel.py +61 -53
- braintrust/test_queue.py +0 -1
- braintrust/test_score.py +1 -3
- braintrust/test_span_components.py +29 -44
- braintrust/util.py +9 -8
- braintrust/version.py +2 -2
- braintrust/wrappers/_anthropic_utils.py +4 -4
- braintrust/wrappers/agno/__init__.py +3 -4
- braintrust/wrappers/agno/agent.py +1 -2
- braintrust/wrappers/agno/function_call.py +1 -2
- braintrust/wrappers/agno/model.py +1 -2
- braintrust/wrappers/agno/team.py +1 -2
- braintrust/wrappers/agno/utils.py +12 -12
- braintrust/wrappers/anthropic.py +7 -8
- braintrust/wrappers/claude_agent_sdk/__init__.py +3 -4
- braintrust/wrappers/claude_agent_sdk/_wrapper.py +29 -27
- braintrust/wrappers/dspy.py +15 -17
- braintrust/wrappers/google_genai/__init__.py +16 -16
- braintrust/wrappers/langchain.py +22 -24
- braintrust/wrappers/litellm.py +4 -3
- braintrust/wrappers/openai.py +15 -15
- braintrust/wrappers/pydantic_ai.py +1204 -0
- braintrust/wrappers/test_agno.py +0 -1
- braintrust/wrappers/test_dspy.py +0 -1
- braintrust/wrappers/test_google_genai.py +2 -3
- braintrust/wrappers/test_litellm.py +0 -1
- braintrust/wrappers/test_oai_attachments.py +322 -0
- braintrust/wrappers/test_pydantic_ai_integration.py +1788 -0
- braintrust/wrappers/{test_pydantic_ai.py → test_pydantic_ai_wrap_openai.py} +1 -2
- {braintrust-0.3.14.dist-info → braintrust-0.4.0.dist-info}/METADATA +3 -2
- braintrust-0.4.0.dist-info/RECORD +120 -0
- braintrust-0.3.14.dist-info/RECORD +0 -117
- {braintrust-0.3.14.dist-info → braintrust-0.4.0.dist-info}/WHEEL +0 -0
- {braintrust-0.3.14.dist-info → braintrust-0.4.0.dist-info}/entry_points.txt +0 -0
- {braintrust-0.3.14.dist-info → braintrust-0.4.0.dist-info}/top_level.txt +0 -0
braintrust/wrappers/test_agno.py
CHANGED
braintrust/wrappers/test_dspy.py
CHANGED
|
@@ -2,12 +2,11 @@ import time
|
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
|
|
4
4
|
import pytest
|
|
5
|
-
from google.genai import types
|
|
6
|
-
from google.genai.client import Client
|
|
7
|
-
|
|
8
5
|
from braintrust import logger
|
|
9
6
|
from braintrust.test_helpers import init_test_logger
|
|
10
7
|
from braintrust.wrappers.google_genai import setup_genai
|
|
8
|
+
from google.genai import types
|
|
9
|
+
from google.genai.client import Client
|
|
11
10
|
|
|
12
11
|
PROJECT_NAME = "test-genai-app"
|
|
13
12
|
MODEL = "gemini-2.0-flash-001"
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""Tests for OpenAI wrapper attachment processing."""
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
import openai
|
|
5
|
+
import pytest
|
|
6
|
+
from braintrust import Attachment, logger, wrap_openai
|
|
7
|
+
from braintrust.test_helpers import init_test_logger
|
|
8
|
+
from braintrust.wrappers.test_utils import assert_metrics_are_valid
|
|
9
|
+
|
|
10
|
+
PROJECT_NAME = "test-project-openai-attachment-processing"
|
|
11
|
+
TEST_MODEL = "gpt-4o-mini"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture(scope="module")
|
|
15
|
+
def vcr_config():
|
|
16
|
+
return {
|
|
17
|
+
"filter_headers": [
|
|
18
|
+
"authorization",
|
|
19
|
+
"openai-organization",
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture
|
|
25
|
+
def memory_logger():
|
|
26
|
+
init_test_logger(PROJECT_NAME)
|
|
27
|
+
with logger._internal_with_memory_background_logger() as bgl:
|
|
28
|
+
yield bgl
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _is_wrapped(client):
|
|
32
|
+
return hasattr(client, "_NamedWrapper__wrapped")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.mark.vcr
|
|
36
|
+
def test_openai_image_data_url_converts_to_attachment(memory_logger):
|
|
37
|
+
"""Test that image data URLs in chat completions are converted to Attachment objects."""
|
|
38
|
+
assert not memory_logger.pop()
|
|
39
|
+
|
|
40
|
+
# Create a simple 1x1 red pixel PNG
|
|
41
|
+
base64_image = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="
|
|
42
|
+
data_url = f"data:image/png;base64,{base64_image}"
|
|
43
|
+
|
|
44
|
+
client = wrap_openai(openai.OpenAI())
|
|
45
|
+
|
|
46
|
+
start = time.time()
|
|
47
|
+
response = client.chat.completions.create(
|
|
48
|
+
model=TEST_MODEL,
|
|
49
|
+
messages=[
|
|
50
|
+
{
|
|
51
|
+
"role": "user",
|
|
52
|
+
"content": [
|
|
53
|
+
{"type": "text", "text": "What color is this image?"},
|
|
54
|
+
{"type": "image_url", "image_url": {"url": data_url}},
|
|
55
|
+
],
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
)
|
|
59
|
+
end = time.time()
|
|
60
|
+
|
|
61
|
+
# Verify we got a successful response
|
|
62
|
+
assert response
|
|
63
|
+
assert response.choices
|
|
64
|
+
assert response.choices[0].message.content
|
|
65
|
+
# The model should be able to see the image
|
|
66
|
+
content = response.choices[0].message.content.lower()
|
|
67
|
+
assert "red" in content or "pink" in content or "color" in content
|
|
68
|
+
|
|
69
|
+
# Verify spans were created
|
|
70
|
+
spans = memory_logger.pop()
|
|
71
|
+
assert len(spans) == 1
|
|
72
|
+
span = spans[0]
|
|
73
|
+
assert span
|
|
74
|
+
|
|
75
|
+
# Verify metrics
|
|
76
|
+
metrics = span["metrics"]
|
|
77
|
+
assert_metrics_are_valid(metrics, start, end)
|
|
78
|
+
assert TEST_MODEL in span["metadata"]["model"]
|
|
79
|
+
assert span["metadata"]["provider"] == "openai"
|
|
80
|
+
|
|
81
|
+
# Verify input contains the attachment
|
|
82
|
+
assert span["input"]
|
|
83
|
+
assert len(span["input"]) == 1
|
|
84
|
+
message_content = span["input"][0]["content"]
|
|
85
|
+
assert len(message_content) == 2
|
|
86
|
+
|
|
87
|
+
# First item should be text
|
|
88
|
+
assert message_content[0]["type"] == "text"
|
|
89
|
+
assert message_content[0]["text"] == "What color is this image?"
|
|
90
|
+
|
|
91
|
+
# Second item should have the image URL converted to Attachment
|
|
92
|
+
assert message_content[1]["type"] == "image_url"
|
|
93
|
+
image_url_value = message_content[1]["image_url"]["url"]
|
|
94
|
+
assert isinstance(image_url_value, Attachment)
|
|
95
|
+
assert image_url_value.reference["type"] == "braintrust_attachment"
|
|
96
|
+
assert image_url_value.reference["content_type"] == "image/png"
|
|
97
|
+
assert image_url_value.reference["filename"] == "image.png"
|
|
98
|
+
assert image_url_value.reference["key"]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@pytest.mark.vcr
|
|
102
|
+
def test_openai_pdf_data_url_converts_to_attachment(memory_logger):
|
|
103
|
+
"""Test that PDF data URLs in chat completions are converted to Attachment objects."""
|
|
104
|
+
assert not memory_logger.pop()
|
|
105
|
+
|
|
106
|
+
# Create a minimal PDF
|
|
107
|
+
base64_pdf = "JVBERi0xLjAKMSAwIG9iago8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIFI+PmVuZG9iagoyIDAgb2JqCjw8L1R5cGUvUGFnZXMvS2lkc1szIDAgUl0vQ291bnQgMT4+ZW5kb2JqCjMgMCBvYmoKPDwvVHlwZS9QYWdlL01lZGlhQm94WzAgMCA2MTIgNzkyXT4+ZW5kb2JqCnhyZWYKMCA0CjAwMDAwMDAwMDAgNjU1MzUgZg0KMDAwMDAwMDAxMCAwMDAwMCBuDQowMDAwMDAwMDUzIDAwMDAwIG4NCjAwMDAwMDAxMDIgMDAwMDAgbg0KdHJhaWxlcgo8PC9TaXplIDQvUm9vdCAxIDAgUj4+CnN0YXJ0eHJlZgoxNDkKJUVPRg=="
|
|
108
|
+
data_url = f"data:application/pdf;base64,{base64_pdf}"
|
|
109
|
+
|
|
110
|
+
client = wrap_openai(openai.OpenAI())
|
|
111
|
+
|
|
112
|
+
start = time.time()
|
|
113
|
+
response = client.chat.completions.create(
|
|
114
|
+
model=TEST_MODEL,
|
|
115
|
+
messages=[
|
|
116
|
+
{
|
|
117
|
+
"role": "user",
|
|
118
|
+
"content": [
|
|
119
|
+
{"type": "text", "text": "What type of document is this?"},
|
|
120
|
+
{
|
|
121
|
+
"type": "file",
|
|
122
|
+
"file": {
|
|
123
|
+
"file_data": data_url,
|
|
124
|
+
"filename": "test.pdf",
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
}
|
|
129
|
+
],
|
|
130
|
+
)
|
|
131
|
+
end = time.time()
|
|
132
|
+
|
|
133
|
+
# Verify we got a successful response
|
|
134
|
+
assert response
|
|
135
|
+
assert response.choices
|
|
136
|
+
assert response.choices[0].message.content
|
|
137
|
+
|
|
138
|
+
# Verify spans were created
|
|
139
|
+
spans = memory_logger.pop()
|
|
140
|
+
assert len(spans) == 1
|
|
141
|
+
span = spans[0]
|
|
142
|
+
assert span
|
|
143
|
+
|
|
144
|
+
# Verify metrics
|
|
145
|
+
metrics = span["metrics"]
|
|
146
|
+
assert_metrics_are_valid(metrics, start, end)
|
|
147
|
+
assert TEST_MODEL in span["metadata"]["model"]
|
|
148
|
+
assert span["metadata"]["provider"] == "openai"
|
|
149
|
+
|
|
150
|
+
# Verify input contains the attachment
|
|
151
|
+
assert span["input"]
|
|
152
|
+
assert len(span["input"]) == 1
|
|
153
|
+
message_content = span["input"][0]["content"]
|
|
154
|
+
assert len(message_content) == 2
|
|
155
|
+
|
|
156
|
+
# First item should be text
|
|
157
|
+
assert message_content[0]["type"] == "text"
|
|
158
|
+
assert message_content[0]["text"] == "What type of document is this?"
|
|
159
|
+
|
|
160
|
+
# Second item should have the file_data converted to Attachment
|
|
161
|
+
assert message_content[1]["type"] == "file"
|
|
162
|
+
file_data_value = message_content[1]["file"]["file_data"]
|
|
163
|
+
assert isinstance(file_data_value, Attachment)
|
|
164
|
+
assert file_data_value.reference["type"] == "braintrust_attachment"
|
|
165
|
+
assert file_data_value.reference["content_type"] == "application/pdf"
|
|
166
|
+
# Should use the provided filename, not a generic one
|
|
167
|
+
assert file_data_value.reference["filename"] == "test.pdf"
|
|
168
|
+
assert file_data_value.reference["key"]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@pytest.mark.vcr
|
|
172
|
+
def test_openai_pdf_data_url_without_filename_uses_fallback(memory_logger):
|
|
173
|
+
"""Test that PDF data URLs without a filename use the generated fallback."""
|
|
174
|
+
assert not memory_logger.pop()
|
|
175
|
+
|
|
176
|
+
# Create a minimal PDF
|
|
177
|
+
base64_pdf = "JVBERi0xLjAKMSAwIG9iago8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIFI+PmVuZG9iagoyIDAgb2JqCjw8L1R5cGUvUGFnZXMvS2lkc1szIDAgUl0vQ291bnQgMT4+ZW5kb2JqCjMgMCBvYmoKPDwvVHlwZS9QYWdlL01lZGlhQm94WzAgMCA2MTIgNzkyXT4+ZW5kb2JqCnhyZWYKMCA0CjAwMDAwMDAwMDAgNjU1MzUgZg0KMDAwMDAwMDAxMCAwMDAwMCBuDQowMDAwMDAwMDUzIDAwMDAwIG4NCjAwMDAwMDAxMDIgMDAwMDAgbg0KdHJhaWxlcgo8PC9TaXplIDQvUm9vdCAxIDAgUj4+CnN0YXJ0eHJlZgoxNDkKJUVPRg=="
|
|
178
|
+
data_url = f"data:application/pdf;base64,{base64_pdf}"
|
|
179
|
+
|
|
180
|
+
client = wrap_openai(openai.OpenAI())
|
|
181
|
+
|
|
182
|
+
start = time.time()
|
|
183
|
+
response = client.chat.completions.create(
|
|
184
|
+
model=TEST_MODEL,
|
|
185
|
+
messages=[
|
|
186
|
+
{
|
|
187
|
+
"role": "user",
|
|
188
|
+
"content": [
|
|
189
|
+
{"type": "text", "text": "What type of document is this?"},
|
|
190
|
+
{
|
|
191
|
+
"type": "file",
|
|
192
|
+
"file": {
|
|
193
|
+
"file_data": data_url,
|
|
194
|
+
# No filename provided - should use fallback
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
}
|
|
199
|
+
],
|
|
200
|
+
)
|
|
201
|
+
end = time.time()
|
|
202
|
+
|
|
203
|
+
# Verify we got a successful response
|
|
204
|
+
assert response
|
|
205
|
+
assert response.choices
|
|
206
|
+
assert response.choices[0].message.content
|
|
207
|
+
|
|
208
|
+
# Verify spans were created
|
|
209
|
+
spans = memory_logger.pop()
|
|
210
|
+
assert len(spans) == 1
|
|
211
|
+
span = spans[0]
|
|
212
|
+
assert span
|
|
213
|
+
|
|
214
|
+
# Verify metrics
|
|
215
|
+
metrics = span["metrics"]
|
|
216
|
+
assert_metrics_are_valid(metrics, start, end)
|
|
217
|
+
assert TEST_MODEL in span["metadata"]["model"]
|
|
218
|
+
assert span["metadata"]["provider"] == "openai"
|
|
219
|
+
|
|
220
|
+
# Verify input contains the attachment
|
|
221
|
+
assert span["input"]
|
|
222
|
+
assert len(span["input"]) == 1
|
|
223
|
+
message_content = span["input"][0]["content"]
|
|
224
|
+
assert len(message_content) == 2
|
|
225
|
+
|
|
226
|
+
# First item should be text
|
|
227
|
+
assert message_content[0]["type"] == "text"
|
|
228
|
+
assert message_content[0]["text"] == "What type of document is this?"
|
|
229
|
+
|
|
230
|
+
# Second item should have the file_data converted to Attachment
|
|
231
|
+
assert message_content[1]["type"] == "file"
|
|
232
|
+
file_data_value = message_content[1]["file"]["file_data"]
|
|
233
|
+
assert isinstance(file_data_value, Attachment)
|
|
234
|
+
assert file_data_value.reference["type"] == "braintrust_attachment"
|
|
235
|
+
assert file_data_value.reference["content_type"] == "application/pdf"
|
|
236
|
+
# Should use the fallback filename since none was provided
|
|
237
|
+
assert file_data_value.reference["filename"] == "document.pdf"
|
|
238
|
+
assert file_data_value.reference["key"]
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@pytest.mark.vcr
|
|
242
|
+
def test_openai_regular_url_preserved(memory_logger):
|
|
243
|
+
"""Test that regular URLs (non-data URLs) are preserved unchanged."""
|
|
244
|
+
assert not memory_logger.pop()
|
|
245
|
+
|
|
246
|
+
# Use a regular URL (not a data URL)
|
|
247
|
+
regular_url = "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"
|
|
248
|
+
|
|
249
|
+
client = wrap_openai(openai.OpenAI())
|
|
250
|
+
|
|
251
|
+
start = time.time()
|
|
252
|
+
response = client.chat.completions.create(
|
|
253
|
+
model=TEST_MODEL,
|
|
254
|
+
messages=[
|
|
255
|
+
{
|
|
256
|
+
"role": "user",
|
|
257
|
+
"content": [
|
|
258
|
+
{"type": "text", "text": "What's in this image?"},
|
|
259
|
+
{"type": "image_url", "image_url": {"url": regular_url}},
|
|
260
|
+
],
|
|
261
|
+
}
|
|
262
|
+
],
|
|
263
|
+
)
|
|
264
|
+
end = time.time()
|
|
265
|
+
|
|
266
|
+
# Verify we got a successful response
|
|
267
|
+
assert response
|
|
268
|
+
assert response.choices
|
|
269
|
+
assert response.choices[0].message.content
|
|
270
|
+
|
|
271
|
+
# Verify spans were created
|
|
272
|
+
spans = memory_logger.pop()
|
|
273
|
+
assert len(spans) == 1
|
|
274
|
+
span = spans[0]
|
|
275
|
+
assert span
|
|
276
|
+
|
|
277
|
+
# Verify metrics
|
|
278
|
+
metrics = span["metrics"]
|
|
279
|
+
assert_metrics_are_valid(metrics, start, end)
|
|
280
|
+
|
|
281
|
+
# Verify input has the URL unchanged (not converted to Attachment)
|
|
282
|
+
assert span["input"]
|
|
283
|
+
message_content = span["input"][0]["content"]
|
|
284
|
+
assert message_content[1]["type"] == "image_url"
|
|
285
|
+
image_url_value = message_content[1]["image_url"]["url"]
|
|
286
|
+
# Regular URLs should NOT be converted to Attachment
|
|
287
|
+
assert isinstance(image_url_value, str)
|
|
288
|
+
assert image_url_value == regular_url
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@pytest.mark.vcr
|
|
292
|
+
def test_openai_unwrapped_client_no_conversion(memory_logger):
|
|
293
|
+
"""Test that unwrapped clients don't process attachments and don't generate spans."""
|
|
294
|
+
assert not memory_logger.pop()
|
|
295
|
+
|
|
296
|
+
# Create a simple image data URL
|
|
297
|
+
base64_image = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="
|
|
298
|
+
data_url = f"data:image/png;base64,{base64_image}"
|
|
299
|
+
|
|
300
|
+
# Use unwrapped client
|
|
301
|
+
client = openai.OpenAI()
|
|
302
|
+
|
|
303
|
+
response = client.chat.completions.create(
|
|
304
|
+
model=TEST_MODEL,
|
|
305
|
+
messages=[
|
|
306
|
+
{
|
|
307
|
+
"role": "user",
|
|
308
|
+
"content": [
|
|
309
|
+
{"type": "text", "text": "What color is this image?"},
|
|
310
|
+
{"type": "image_url", "image_url": {"url": data_url}},
|
|
311
|
+
],
|
|
312
|
+
}
|
|
313
|
+
],
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Verify we got a successful response
|
|
317
|
+
assert response
|
|
318
|
+
assert response.choices
|
|
319
|
+
assert response.choices[0].message.content
|
|
320
|
+
|
|
321
|
+
# No spans should be generated with unwrapped client
|
|
322
|
+
assert not memory_logger.pop()
|