Skip to content

Commit 9548ae3

Browse files
authored
feat: add tags support to SDK builder classes (#33)
1 parent 97da34d commit 9548ae3

5 files changed

Lines changed: 234 additions & 67 deletions

File tree

py/src/braintrust/cli/push.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,8 @@ def _collect_function_function_defs(
251251
}
252252
if f.metadata is not None:
253253
j["metadata"] = f.metadata
254+
if f.tags is not None:
255+
j["tags"] = list(f.tags)
254256
if f.parameters is None:
255257
raise ValueError(f"Function {f.name} has no supplied parameters")
256258
j["function_schema"] = {

py/src/braintrust/cli/test_push.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""Tests for push command serialization."""
2+
3+
import pytest
4+
5+
pydantic = pytest.importorskip("pydantic")
6+
7+
from ..framework2 import (
8+
global_,
9+
projects,
10+
)
11+
from .push import _collect_function_function_defs
12+
13+
14+
class ToolInput(pydantic.BaseModel):
15+
value: int
16+
17+
18+
@pytest.fixture(autouse=True)
19+
def clear_global_state():
20+
global_.functions.clear()
21+
global_.prompts.clear()
22+
yield
23+
global_.functions.clear()
24+
global_.prompts.clear()
25+
26+
27+
class TestPushMetadata:
28+
"""Tests for metadata in push command serialization."""
29+
30+
def test_collect_function_function_defs_includes_metadata(self, mock_project_ids):
31+
project = projects.create("test-project")
32+
metadata = {"version": "1.0", "author": "test"}
33+
34+
tool = project.tools.create(
35+
handler=lambda x: x,
36+
name="test-tool",
37+
parameters=ToolInput,
38+
metadata=metadata,
39+
)
40+
global_.functions.append(tool)
41+
42+
functions = []
43+
_collect_function_function_defs(mock_project_ids, functions, "bundle-123", "error")
44+
45+
assert len(functions) == 1
46+
assert functions[0]["metadata"] == metadata
47+
assert functions[0]["name"] == "test-tool"
48+
49+
def test_collect_function_function_defs_excludes_metadata_when_none(self, mock_project_ids):
50+
project = projects.create("test-project")
51+
52+
tool = project.tools.create(
53+
handler=lambda x: x,
54+
name="test-tool",
55+
parameters=ToolInput,
56+
)
57+
global_.functions.append(tool)
58+
59+
functions = []
60+
_collect_function_function_defs(mock_project_ids, functions, "bundle-123", "error")
61+
62+
assert len(functions) == 1
63+
assert "metadata" not in functions[0]
64+
65+
66+
class TestPushTags:
67+
"""Tests for tags in push command serialization."""
68+
69+
def test_collect_function_function_defs_includes_tags(self, mock_project_ids):
70+
project = projects.create("test-project")
71+
tags = ["production", "v1"]
72+
73+
tool = project.tools.create(
74+
handler=lambda x: x,
75+
name="test-tool",
76+
parameters=ToolInput,
77+
tags=tags,
78+
)
79+
global_.functions.append(tool)
80+
81+
functions = []
82+
_collect_function_function_defs(mock_project_ids, functions, "bundle-123", "error")
83+
84+
assert len(functions) == 1
85+
assert functions[0]["tags"] == ["production", "v1"]
86+
87+
def test_collect_function_function_defs_excludes_tags_when_none(self, mock_project_ids):
88+
project = projects.create("test-project")
89+
90+
tool = project.tools.create(
91+
handler=lambda x: x,
92+
name="test-tool",
93+
parameters=ToolInput,
94+
)
95+
global_.functions.append(tool)
96+
97+
functions = []
98+
_collect_function_function_defs(mock_project_ids, functions, "bundle-123", "error")
99+
100+
assert len(functions) == 1
101+
assert "tags" not in functions[0]

py/src/braintrust/conftest.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import os
2+
from unittest.mock import MagicMock
23

34
import pytest
5+
from braintrust.framework2 import ProjectIdCache
46

57

68
def _patch_vcr_aiohttp_stubs():
@@ -107,6 +109,13 @@ async def _patched_record_response(cassette, vcr_request, response):
107109
_patch_vcr_aiohttp_stubs()
108110

109111

112+
@pytest.fixture
113+
def mock_project_ids():
114+
mock = MagicMock(spec=ProjectIdCache)
115+
mock.get.return_value = "project-123"
116+
return mock
117+
118+
110119
@pytest.fixture(autouse=True)
111120
def override_app_url_for_tests():
112121
"""

py/src/braintrust/framework2.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import dataclasses
22
import json
3-
from collections.abc import Callable
3+
from collections.abc import Callable, Sequence
44
from typing import Any, overload
55

66
import slugify
@@ -53,6 +53,7 @@ class CodeFunction:
5353
returns: Any
5454
if_exists: IfExists | None
5555
metadata: dict[str, Any] | None = None
56+
tags: Sequence[str] | None = None
5657

5758

5859
@dataclasses.dataclass
@@ -69,6 +70,7 @@ class CodePrompt:
6970
id: str | None
7071
if_exists: IfExists | None
7172
metadata: dict[str, Any] | None = None
73+
tags: Sequence[str] | None = None
7274

7375
def to_function_definition(self, if_exists: IfExists | None, project_ids: ProjectIdCache) -> dict[str, Any]:
7476
prompt_data = self.prompt
@@ -102,6 +104,8 @@ def to_function_definition(self, if_exists: IfExists | None, project_ids: Projec
102104
j["function_type"] = self.function_type
103105
if self.metadata is not None:
104106
j["metadata"] = self.metadata
107+
if self.tags is not None:
108+
j["tags"] = list(self.tags)
105109

106110
return j
107111

@@ -124,6 +128,7 @@ def create(
124128
returns: Any = None,
125129
if_exists: IfExists | None = None,
126130
metadata: dict[str, Any] | None = None,
131+
tags: Sequence[str] | None = None,
127132
) -> CodeFunction:
128133
"""Creates a tool.
129134
@@ -136,6 +141,7 @@ def create(
136141
returns: The tool's output schema, as a Pydantic model.
137142
if_exists: What to do if the tool already exists.
138143
metadata: Custom metadata to attach to the tool.
144+
tags: A list of tags for the tool.
139145
140146
Returns:
141147
A handle to the created tool, that can be used in a prompt.
@@ -160,6 +166,7 @@ def create(
160166
returns=returns,
161167
if_exists=if_exists,
162168
metadata=metadata,
169+
tags=tags,
163170
)
164171
self.project.add_code_function(f)
165172
return f
@@ -186,6 +193,7 @@ def create(
186193
tools: list[CodeFunction | SavedFunctionId | ToolFunctionDefinition] | None = None,
187194
if_exists: IfExists | None = None,
188195
metadata: dict[str, Any] | None = None,
196+
tags: Sequence[str] | None = None,
189197
) -> CodePrompt: ...
190198

191199
@overload # messages only, no prompt
@@ -202,6 +210,7 @@ def create(
202210
tools: list[CodeFunction | SavedFunctionId | ToolFunctionDefinition] | None = None,
203211
if_exists: IfExists | None = None,
204212
metadata: dict[str, Any] | None = None,
213+
tags: Sequence[str] | None = None,
205214
) -> CodePrompt: ...
206215

207216
def create(
@@ -218,6 +227,7 @@ def create(
218227
tools: list[CodeFunction | SavedFunctionId | ToolFunctionDefinition] | None = None,
219228
if_exists: IfExists | None = None,
220229
metadata: dict[str, Any] | None = None,
230+
tags: Sequence[str] | None = None,
221231
):
222232
"""Creates a prompt.
223233
@@ -233,6 +243,7 @@ def create(
233243
tools: The tools to use for the prompt.
234244
if_exists: What to do if the prompt already exists.
235245
metadata: Custom metadata to attach to the prompt.
246+
tags: A list of tags for the prompt.
236247
"""
237248
self._task_counter += 1
238249
if not name:
@@ -282,6 +293,7 @@ def create(
282293
id=id,
283294
if_exists=if_exists,
284295
metadata=metadata,
296+
tags=tags,
285297
)
286298
self.project.add_prompt(p)
287299
return p
@@ -304,6 +316,7 @@ def create(
304316
description: str | None = None,
305317
if_exists: IfExists | None = None,
306318
metadata: dict[str, Any] | None = None,
319+
tags: Sequence[str] | None = None,
307320
handler: Callable[..., Any],
308321
parameters: Any,
309322
returns: Any = None,
@@ -319,6 +332,7 @@ def create(
319332
description: str | None = None,
320333
if_exists: IfExists | None = None,
321334
metadata: dict[str, Any] | None = None,
335+
tags: Sequence[str] | None = None,
322336
prompt: str,
323337
model: str,
324338
params: ModelParams | None = None,
@@ -336,6 +350,7 @@ def create(
336350
description: str | None = None,
337351
if_exists: IfExists | None = None,
338352
metadata: dict[str, Any] | None = None,
353+
tags: Sequence[str] | None = None,
339354
messages: list[ChatCompletionMessageParam],
340355
model: str,
341356
params: ModelParams | None = None,
@@ -351,6 +366,7 @@ def create(
351366
description: str | None = None,
352367
if_exists: IfExists | None = None,
353368
metadata: dict[str, Any] | None = None,
369+
tags: Sequence[str] | None = None,
354370
# Code scorer params.
355371
handler: Callable[..., Any] | None = None,
356372
parameters: Any = None,
@@ -371,6 +387,7 @@ def create(
371387
description: The description of the scorer.
372388
if_exists: What to do if the scorer already exists.
373389
metadata: Custom metadata to attach to the scorer.
390+
tags: A list of tags for the scorer.
374391
375392
The remaining args are mutually exclusive; that is,
376393
the function will only accept args from one of the following overloads.
@@ -410,6 +427,7 @@ def create(
410427
returns=returns,
411428
if_exists=if_exists,
412429
metadata=metadata,
430+
tags=tags,
413431
)
414432
self.project.add_code_function(f)
415433
return f
@@ -449,6 +467,7 @@ def create(
449467
id=None,
450468
if_exists=if_exists,
451469
metadata=metadata,
470+
tags=tags,
452471
)
453472
self.project.add_prompt(p)
454473
return p

0 commit comments

Comments
 (0)