braintrust 0.3.14__py3-none-any.whl → 0.3.15__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 +596 -72
- braintrust/conftest.py +1 -0
- braintrust/functions/invoke.py +35 -2
- braintrust/generated_types.py +15 -1
- braintrust/oai.py +88 -6
- braintrust/version.py +2 -2
- braintrust/wrappers/pydantic_ai.py +1203 -0
- 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.3.15.dist-info}/METADATA +1 -1
- {braintrust-0.3.14.dist-info → braintrust-0.3.15.dist-info}/RECORD +16 -13
- {braintrust-0.3.14.dist-info → braintrust-0.3.15.dist-info}/WHEEL +0 -0
- {braintrust-0.3.14.dist-info → braintrust-0.3.15.dist-info}/entry_points.txt +0 -0
- {braintrust-0.3.14.dist-info → braintrust-0.3.15.dist-info}/top_level.txt +0 -0
braintrust/conftest.py
CHANGED
|
@@ -36,6 +36,7 @@ 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
|
+
os.environ.setdefault("OPENAI_API_KEY", "sk-test-dummy-api-key-for-vcr-tests")
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
@pytest.fixture(autouse=True)
|
braintrust/functions/invoke.py
CHANGED
|
@@ -1,14 +1,31 @@
|
|
|
1
|
-
from typing import Any, Dict, List, Literal, Optional, TypeVar, Union, overload
|
|
1
|
+
from typing import Any, Dict, List, Literal, Optional, TypedDict, TypeVar, Union, overload
|
|
2
2
|
|
|
3
3
|
from sseclient import SSEClient
|
|
4
4
|
|
|
5
|
+
from .._generated_types import InvokeContext
|
|
5
6
|
from ..logger import Exportable, get_span_parent_object, login, proxy_conn
|
|
6
7
|
from ..util import response_raise_for_status
|
|
7
8
|
from .constants import INVOKE_API_VERSION
|
|
8
9
|
from .stream import BraintrustInvokeError, BraintrustStream
|
|
9
10
|
|
|
10
11
|
T = TypeVar("T")
|
|
11
|
-
ModeType = Literal["auto", "parallel"]
|
|
12
|
+
ModeType = Literal["auto", "parallel", "json", "text"]
|
|
13
|
+
ObjectType = Literal["project_logs", "experiment", "dataset", "playground_logs"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SpanScope(TypedDict):
|
|
17
|
+
"""Scope for operating on a single span."""
|
|
18
|
+
|
|
19
|
+
type: Literal["span"]
|
|
20
|
+
id: str
|
|
21
|
+
root_span_id: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TraceScope(TypedDict):
|
|
25
|
+
"""Scope for operating on an entire trace."""
|
|
26
|
+
|
|
27
|
+
type: Literal["trace"]
|
|
28
|
+
root_span_id: str
|
|
12
29
|
|
|
13
30
|
|
|
14
31
|
@overload
|
|
@@ -19,11 +36,13 @@ def invoke(
|
|
|
19
36
|
prompt_session_id: Optional[str] = None,
|
|
20
37
|
prompt_session_function_id: Optional[str] = None,
|
|
21
38
|
project_name: Optional[str] = None,
|
|
39
|
+
project_id: Optional[str] = None,
|
|
22
40
|
slug: Optional[str] = None,
|
|
23
41
|
global_function: Optional[str] = None,
|
|
24
42
|
# arguments to the function
|
|
25
43
|
input: Any = None,
|
|
26
44
|
messages: Optional[List[Any]] = None,
|
|
45
|
+
context: Optional[InvokeContext] = None,
|
|
27
46
|
metadata: Optional[Dict[str, Any]] = None,
|
|
28
47
|
tags: Optional[List[str]] = None,
|
|
29
48
|
parent: Optional[Union[Exportable, str]] = None,
|
|
@@ -45,11 +64,13 @@ def invoke(
|
|
|
45
64
|
prompt_session_id: Optional[str] = None,
|
|
46
65
|
prompt_session_function_id: Optional[str] = None,
|
|
47
66
|
project_name: Optional[str] = None,
|
|
67
|
+
project_id: Optional[str] = None,
|
|
48
68
|
slug: Optional[str] = None,
|
|
49
69
|
global_function: Optional[str] = None,
|
|
50
70
|
# arguments to the function
|
|
51
71
|
input: Any = None,
|
|
52
72
|
messages: Optional[List[Any]] = None,
|
|
73
|
+
context: Optional[InvokeContext] = None,
|
|
53
74
|
metadata: Optional[Dict[str, Any]] = None,
|
|
54
75
|
tags: Optional[List[str]] = None,
|
|
55
76
|
parent: Optional[Union[Exportable, str]] = None,
|
|
@@ -70,11 +91,13 @@ def invoke(
|
|
|
70
91
|
prompt_session_id: Optional[str] = None,
|
|
71
92
|
prompt_session_function_id: Optional[str] = None,
|
|
72
93
|
project_name: Optional[str] = None,
|
|
94
|
+
project_id: Optional[str] = None,
|
|
73
95
|
slug: Optional[str] = None,
|
|
74
96
|
global_function: Optional[str] = None,
|
|
75
97
|
# arguments to the function
|
|
76
98
|
input: Any = None,
|
|
77
99
|
messages: Optional[List[Any]] = None,
|
|
100
|
+
context: Optional[InvokeContext] = None,
|
|
78
101
|
metadata: Optional[Dict[str, Any]] = None,
|
|
79
102
|
tags: Optional[List[str]] = None,
|
|
80
103
|
parent: Optional[Union[Exportable, str]] = None,
|
|
@@ -93,6 +116,8 @@ def invoke(
|
|
|
93
116
|
Args:
|
|
94
117
|
input: The input to the function. This will be logged as the `input` field in the span.
|
|
95
118
|
messages: Additional OpenAI-style messages to add to the prompt (only works for llm functions).
|
|
119
|
+
context: Context for functions that operate on spans/traces (e.g., facets). Should contain
|
|
120
|
+
`object_type`, `object_id`, and `scope` fields.
|
|
96
121
|
metadata: Additional metadata to add to the span. This will be logged as the `metadata` field in the span.
|
|
97
122
|
It will also be available as the {{metadata}} field in the prompt and as the `metadata` argument
|
|
98
123
|
to the function.
|
|
@@ -118,6 +143,8 @@ def invoke(
|
|
|
118
143
|
prompt_session_id: The ID of the prompt session to invoke the function from.
|
|
119
144
|
prompt_session_function_id: The ID of the function in the prompt session to invoke.
|
|
120
145
|
project_name: The name of the project containing the function to invoke.
|
|
146
|
+
project_id: The ID of the project to use for execution context (API keys, project defaults, etc.).
|
|
147
|
+
This is not the project the function belongs to, but the project context for the invocation.
|
|
121
148
|
slug: The slug of the function to invoke.
|
|
122
149
|
global_function: The name of the global function to invoke.
|
|
123
150
|
|
|
@@ -161,12 +188,18 @@ def invoke(
|
|
|
161
188
|
)
|
|
162
189
|
if messages is not None:
|
|
163
190
|
request["messages"] = messages
|
|
191
|
+
if context is not None:
|
|
192
|
+
request["context"] = context
|
|
164
193
|
if mode is not None:
|
|
165
194
|
request["mode"] = mode
|
|
166
195
|
if strict is not None:
|
|
167
196
|
request["strict"] = strict
|
|
168
197
|
|
|
169
198
|
headers = {"Accept": "text/event-stream" if stream else "application/json"}
|
|
199
|
+
if project_id is not None:
|
|
200
|
+
headers["x-bt-project-id"] = project_id
|
|
201
|
+
if org_name is not None:
|
|
202
|
+
headers["x-bt-org-name"] = org_name
|
|
170
203
|
|
|
171
204
|
resp = proxy_conn().post("function/invoke", json=request, headers=headers, stream=stream)
|
|
172
205
|
if resp.status_code == 500:
|
braintrust/generated_types.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Auto-generated file (internal git SHA
|
|
1
|
+
"""Auto-generated file (internal git SHA 437eb5379a737f70dec98033fccf81de43e8e177) -- do not modify"""
|
|
2
2
|
|
|
3
3
|
from ._generated_types import (
|
|
4
4
|
Acl,
|
|
@@ -32,6 +32,7 @@ from ._generated_types import (
|
|
|
32
32
|
ExperimentEvent,
|
|
33
33
|
ExtendedSavedFunctionId,
|
|
34
34
|
ExternalAttachmentReference,
|
|
35
|
+
FacetData,
|
|
35
36
|
Function,
|
|
36
37
|
FunctionData,
|
|
37
38
|
FunctionFormat,
|
|
@@ -47,10 +48,14 @@ from ._generated_types import (
|
|
|
47
48
|
GraphNode,
|
|
48
49
|
Group,
|
|
49
50
|
IfExists,
|
|
51
|
+
InvokeContext,
|
|
50
52
|
InvokeFunction,
|
|
51
53
|
InvokeParent,
|
|
54
|
+
InvokeScope,
|
|
55
|
+
MCPServer,
|
|
52
56
|
MessageRole,
|
|
53
57
|
ModelParams,
|
|
58
|
+
NullableSavedFunctionId,
|
|
54
59
|
ObjectReference,
|
|
55
60
|
ObjectReferenceNullish,
|
|
56
61
|
OnlineScoreConfig,
|
|
@@ -86,11 +91,13 @@ from ._generated_types import (
|
|
|
86
91
|
ServiceToken,
|
|
87
92
|
SpanAttributes,
|
|
88
93
|
SpanIFrame,
|
|
94
|
+
SpanScope,
|
|
89
95
|
SpanType,
|
|
90
96
|
SSEConsoleEventData,
|
|
91
97
|
SSEProgressEventData,
|
|
92
98
|
StreamingMode,
|
|
93
99
|
ToolFunctionDefinition,
|
|
100
|
+
TraceScope,
|
|
94
101
|
UploadStatus,
|
|
95
102
|
User,
|
|
96
103
|
View,
|
|
@@ -131,6 +138,7 @@ __all__ = [
|
|
|
131
138
|
"ExperimentEvent",
|
|
132
139
|
"ExtendedSavedFunctionId",
|
|
133
140
|
"ExternalAttachmentReference",
|
|
141
|
+
"FacetData",
|
|
134
142
|
"Function",
|
|
135
143
|
"FunctionData",
|
|
136
144
|
"FunctionFormat",
|
|
@@ -146,10 +154,14 @@ __all__ = [
|
|
|
146
154
|
"GraphNode",
|
|
147
155
|
"Group",
|
|
148
156
|
"IfExists",
|
|
157
|
+
"InvokeContext",
|
|
149
158
|
"InvokeFunction",
|
|
150
159
|
"InvokeParent",
|
|
160
|
+
"InvokeScope",
|
|
161
|
+
"MCPServer",
|
|
151
162
|
"MessageRole",
|
|
152
163
|
"ModelParams",
|
|
164
|
+
"NullableSavedFunctionId",
|
|
153
165
|
"ObjectReference",
|
|
154
166
|
"ObjectReferenceNullish",
|
|
155
167
|
"OnlineScoreConfig",
|
|
@@ -187,9 +199,11 @@ __all__ = [
|
|
|
187
199
|
"ServiceToken",
|
|
188
200
|
"SpanAttributes",
|
|
189
201
|
"SpanIFrame",
|
|
202
|
+
"SpanScope",
|
|
190
203
|
"SpanType",
|
|
191
204
|
"StreamingMode",
|
|
192
205
|
"ToolFunctionDefinition",
|
|
206
|
+
"TraceScope",
|
|
193
207
|
"UploadStatus",
|
|
194
208
|
"User",
|
|
195
209
|
"View",
|
braintrust/oai.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import abc
|
|
2
|
+
import base64
|
|
3
|
+
import re
|
|
2
4
|
import time
|
|
3
|
-
from typing import Any, Callable, Dict, List, Optional
|
|
5
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
4
6
|
|
|
5
|
-
from .logger import Span, start_span
|
|
7
|
+
from .logger import Attachment, Span, start_span
|
|
6
8
|
from .span_types import SpanTypeAttribute
|
|
7
9
|
from .util import merge_dicts
|
|
8
10
|
|
|
@@ -68,6 +70,75 @@ def log_headers(response: Any, span: Span):
|
|
|
68
70
|
)
|
|
69
71
|
|
|
70
72
|
|
|
73
|
+
def _convert_data_url_to_attachment(data_url: str, filename: Optional[str] = None) -> Union[Attachment, str]:
|
|
74
|
+
"""Helper function to convert data URL to an Attachment."""
|
|
75
|
+
data_url_match = re.match(r"^data:([^;]+);base64,(.+)$", data_url)
|
|
76
|
+
if not data_url_match:
|
|
77
|
+
return data_url
|
|
78
|
+
|
|
79
|
+
mime_type, base64_data = data_url_match.groups()
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
binary_data = base64.b64decode(base64_data)
|
|
83
|
+
|
|
84
|
+
if filename is None:
|
|
85
|
+
extension = mime_type.split("/")[1] if "/" in mime_type else "bin"
|
|
86
|
+
prefix = "image" if mime_type.startswith("image/") else "document"
|
|
87
|
+
filename = f"{prefix}.{extension}"
|
|
88
|
+
|
|
89
|
+
attachment = Attachment(data=binary_data, filename=filename, content_type=mime_type)
|
|
90
|
+
|
|
91
|
+
return attachment
|
|
92
|
+
except Exception:
|
|
93
|
+
return data_url
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _process_attachments_in_input(input_data: Any) -> Any:
|
|
97
|
+
"""Process input to convert data URL images and base64 documents to Attachment objects."""
|
|
98
|
+
if isinstance(input_data, list):
|
|
99
|
+
return [_process_attachments_in_input(item) for item in input_data]
|
|
100
|
+
|
|
101
|
+
if isinstance(input_data, dict):
|
|
102
|
+
# Check for OpenAI's image_url format with data URLs
|
|
103
|
+
if (
|
|
104
|
+
input_data.get("type") == "image_url"
|
|
105
|
+
and isinstance(input_data.get("image_url"), dict)
|
|
106
|
+
and isinstance(input_data["image_url"].get("url"), str)
|
|
107
|
+
):
|
|
108
|
+
processed_url = _convert_data_url_to_attachment(input_data["image_url"]["url"])
|
|
109
|
+
return {
|
|
110
|
+
**input_data,
|
|
111
|
+
"image_url": {
|
|
112
|
+
**input_data["image_url"],
|
|
113
|
+
"url": processed_url,
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Check for OpenAI's file format with data URL (e.g., PDFs)
|
|
118
|
+
if (
|
|
119
|
+
input_data.get("type") == "file"
|
|
120
|
+
and isinstance(input_data.get("file"), dict)
|
|
121
|
+
and isinstance(input_data["file"].get("file_data"), str)
|
|
122
|
+
):
|
|
123
|
+
file_filename = input_data["file"].get("filename")
|
|
124
|
+
processed_file_data = _convert_data_url_to_attachment(
|
|
125
|
+
input_data["file"]["file_data"],
|
|
126
|
+
filename=file_filename if isinstance(file_filename, str) else None,
|
|
127
|
+
)
|
|
128
|
+
return {
|
|
129
|
+
**input_data,
|
|
130
|
+
"file": {
|
|
131
|
+
**input_data["file"],
|
|
132
|
+
"file_data": processed_file_data,
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# Recursively process nested objects
|
|
137
|
+
return {key: _process_attachments_in_input(value) for key, value in input_data.items()}
|
|
138
|
+
|
|
139
|
+
return input_data
|
|
140
|
+
|
|
141
|
+
|
|
71
142
|
class ChatCompletionWrapper:
|
|
72
143
|
def __init__(self, create_fn: Optional[Callable[..., Any]], acreate_fn: Optional[Callable[..., Any]]):
|
|
73
144
|
self.create_fn = create_fn
|
|
@@ -190,10 +261,14 @@ class ChatCompletionWrapper:
|
|
|
190
261
|
# Then, copy the rest of the params
|
|
191
262
|
params = prettify_params(params)
|
|
192
263
|
messages = params.pop("messages", None)
|
|
264
|
+
|
|
265
|
+
# Process attachments in input (convert data URLs to Attachment objects)
|
|
266
|
+
processed_input = _process_attachments_in_input(messages)
|
|
267
|
+
|
|
193
268
|
return merge_dicts(
|
|
194
269
|
ret,
|
|
195
270
|
{
|
|
196
|
-
"input":
|
|
271
|
+
"input": processed_input,
|
|
197
272
|
"metadata": {**params, "provider": "openai"},
|
|
198
273
|
},
|
|
199
274
|
)
|
|
@@ -379,10 +454,14 @@ class ResponseWrapper:
|
|
|
379
454
|
# Then, copy the rest of the params
|
|
380
455
|
params = prettify_params(params)
|
|
381
456
|
input_data = params.pop("input", None)
|
|
457
|
+
|
|
458
|
+
# Process attachments in input (convert data URLs to Attachment objects)
|
|
459
|
+
processed_input = _process_attachments_in_input(input_data)
|
|
460
|
+
|
|
382
461
|
return merge_dicts(
|
|
383
462
|
ret,
|
|
384
463
|
{
|
|
385
|
-
"input":
|
|
464
|
+
"input": processed_input,
|
|
386
465
|
"metadata": {**params, "provider": "openai"},
|
|
387
466
|
},
|
|
388
467
|
)
|
|
@@ -540,12 +619,15 @@ class BaseWrapper(abc.ABC):
|
|
|
540
619
|
ret = params.pop("span_info", {})
|
|
541
620
|
|
|
542
621
|
params = prettify_params(params)
|
|
543
|
-
|
|
622
|
+
input_data = params.pop("input", None)
|
|
623
|
+
|
|
624
|
+
# Process attachments in input (convert data URLs to Attachment objects)
|
|
625
|
+
processed_input = _process_attachments_in_input(input_data)
|
|
544
626
|
|
|
545
627
|
return merge_dicts(
|
|
546
628
|
ret,
|
|
547
629
|
{
|
|
548
|
-
"input":
|
|
630
|
+
"input": processed_input,
|
|
549
631
|
"metadata": {**params, "provider": "openai"},
|
|
550
632
|
},
|
|
551
633
|
)
|
braintrust/version.py
CHANGED