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.
Files changed (83) hide show
  1. braintrust/__init__.py +4 -0
  2. braintrust/_generated_types.py +1200 -611
  3. braintrust/audit.py +2 -2
  4. braintrust/cli/eval.py +6 -7
  5. braintrust/cli/push.py +11 -11
  6. braintrust/conftest.py +1 -0
  7. braintrust/context.py +12 -17
  8. braintrust/contrib/temporal/__init__.py +16 -27
  9. braintrust/contrib/temporal/test_temporal.py +8 -3
  10. braintrust/devserver/auth.py +8 -8
  11. braintrust/devserver/cache.py +3 -4
  12. braintrust/devserver/cors.py +8 -7
  13. braintrust/devserver/dataset.py +3 -5
  14. braintrust/devserver/eval_hooks.py +7 -6
  15. braintrust/devserver/schemas.py +22 -19
  16. braintrust/devserver/server.py +19 -12
  17. braintrust/devserver/test_cached_login.py +4 -4
  18. braintrust/framework.py +128 -140
  19. braintrust/framework2.py +88 -87
  20. braintrust/functions/invoke.py +93 -53
  21. braintrust/functions/stream.py +3 -2
  22. braintrust/generated_types.py +17 -1
  23. braintrust/git_fields.py +11 -11
  24. braintrust/gitutil.py +2 -3
  25. braintrust/graph_util.py +10 -10
  26. braintrust/id_gen.py +2 -2
  27. braintrust/logger.py +346 -357
  28. braintrust/merge_row_batch.py +10 -9
  29. braintrust/oai.py +107 -24
  30. braintrust/otel/__init__.py +49 -49
  31. braintrust/otel/context.py +16 -30
  32. braintrust/otel/test_distributed_tracing.py +14 -11
  33. braintrust/otel/test_otel_bt_integration.py +32 -31
  34. braintrust/parameters.py +8 -8
  35. braintrust/prompt.py +14 -14
  36. braintrust/prompt_cache/disk_cache.py +5 -4
  37. braintrust/prompt_cache/lru_cache.py +3 -2
  38. braintrust/prompt_cache/prompt_cache.py +13 -14
  39. braintrust/queue.py +4 -4
  40. braintrust/score.py +4 -4
  41. braintrust/serializable_data_class.py +4 -4
  42. braintrust/span_identifier_v1.py +1 -2
  43. braintrust/span_identifier_v2.py +3 -4
  44. braintrust/span_identifier_v3.py +23 -20
  45. braintrust/span_identifier_v4.py +34 -25
  46. braintrust/test_framework.py +16 -6
  47. braintrust/test_helpers.py +5 -5
  48. braintrust/test_id_gen.py +2 -3
  49. braintrust/test_otel.py +61 -53
  50. braintrust/test_queue.py +0 -1
  51. braintrust/test_score.py +1 -3
  52. braintrust/test_span_components.py +29 -44
  53. braintrust/util.py +9 -8
  54. braintrust/version.py +2 -2
  55. braintrust/wrappers/_anthropic_utils.py +4 -4
  56. braintrust/wrappers/agno/__init__.py +3 -4
  57. braintrust/wrappers/agno/agent.py +1 -2
  58. braintrust/wrappers/agno/function_call.py +1 -2
  59. braintrust/wrappers/agno/model.py +1 -2
  60. braintrust/wrappers/agno/team.py +1 -2
  61. braintrust/wrappers/agno/utils.py +12 -12
  62. braintrust/wrappers/anthropic.py +7 -8
  63. braintrust/wrappers/claude_agent_sdk/__init__.py +3 -4
  64. braintrust/wrappers/claude_agent_sdk/_wrapper.py +29 -27
  65. braintrust/wrappers/dspy.py +15 -17
  66. braintrust/wrappers/google_genai/__init__.py +16 -16
  67. braintrust/wrappers/langchain.py +22 -24
  68. braintrust/wrappers/litellm.py +4 -3
  69. braintrust/wrappers/openai.py +15 -15
  70. braintrust/wrappers/pydantic_ai.py +1204 -0
  71. braintrust/wrappers/test_agno.py +0 -1
  72. braintrust/wrappers/test_dspy.py +0 -1
  73. braintrust/wrappers/test_google_genai.py +2 -3
  74. braintrust/wrappers/test_litellm.py +0 -1
  75. braintrust/wrappers/test_oai_attachments.py +322 -0
  76. braintrust/wrappers/test_pydantic_ai_integration.py +1788 -0
  77. braintrust/wrappers/{test_pydantic_ai.py → test_pydantic_ai_wrap_openai.py} +1 -2
  78. {braintrust-0.3.14.dist-info → braintrust-0.4.0.dist-info}/METADATA +3 -2
  79. braintrust-0.4.0.dist-info/RECORD +120 -0
  80. braintrust-0.3.14.dist-info/RECORD +0 -117
  81. {braintrust-0.3.14.dist-info → braintrust-0.4.0.dist-info}/WHEEL +0 -0
  82. {braintrust-0.3.14.dist-info → braintrust-0.4.0.dist-info}/entry_points.txt +0 -0
  83. {braintrust-0.3.14.dist-info → braintrust-0.4.0.dist-info}/top_level.txt +0 -0
@@ -5,7 +5,6 @@
5
5
  # pyright: reportUnknownVariableType=false
6
6
  # pyright: reportUnknownArgumentType=false
7
7
  import pytest
8
-
9
8
  from braintrust import logger
10
9
  from braintrust.test_helpers import init_test_logger
11
10
  from braintrust.wrappers.agno import setup_agno
@@ -4,7 +4,6 @@ Tests for DSPy integration with Braintrust.
4
4
 
5
5
  import dspy
6
6
  import pytest
7
-
8
7
  from braintrust import logger
9
8
  from braintrust.test_helpers import init_test_logger
10
9
  from braintrust.wrappers.dspy import BraintrustDSpyCallback
@@ -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"
@@ -3,7 +3,6 @@ import time
3
3
 
4
4
  import litellm
5
5
  import pytest
6
-
7
6
  from braintrust import logger
8
7
  from braintrust.test_helpers import assert_dict_matches, init_test_logger
9
8
  from braintrust.wrappers.litellm import wrap_litellm
@@ -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()