Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions py/src/braintrust/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,14 +215,32 @@ def is_eval_parameter_schema(schema: Any) -> bool:
return True


def _strip_none_values(value: Any) -> Any:
"""Recursively drop dict entries whose value is ``None``.

Prompt parameter defaults are serialized from Python prompt dataclasses,
which preserve explicit ``None`` values for optional fields (e.g. a message's
``name``/``function_call``/``tool_calls`` or a chat block's ``tools``). The
JS/UI evaluator manifest schema treats those fields as optional-when-absent
rather than nullable, so an explicit ``null`` fails validation and the remote
eval never appears in the playground. Stripping ``None`` keeps the emitted
default aligned with what the UI schema expects.
"""
if isinstance(value, dict):
return {key: _strip_none_values(item) for key, item in value.items() if item is not None}
if isinstance(value, list):
return [_strip_none_values(item) for item in value]
return value


def _prompt_data_to_dict(
prompt_data: PromptDataDict | PromptData | None,
) -> dict[str, Any] | None:
if prompt_data is None:
return None
if isinstance(prompt_data, PromptData):
return prompt_data.as_dict()
return dict(prompt_data)
return _strip_none_values(prompt_data.as_dict())
return _strip_none_values(dict(prompt_data))


def _create_prompt(name: str, prompt_data: dict[str, Any]) -> "Prompt":
Expand Down
72 changes: 72 additions & 0 deletions py/src/braintrust/test_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
RemoteEvalParameters,
parameters_to_json_schema,
serialize_eval_parameters,
serialize_remote_eval_parameters_container,
validate_parameters,
)
from braintrust.prompt import PromptChatBlock, PromptData, PromptMessage


HAS_PYDANTIC = importlib.util.find_spec("pydantic") is not None
Expand Down Expand Up @@ -563,3 +565,73 @@ def test_parameters_to_json_schema_does_not_mark_passthrough_values_required():
)

assert "required" not in schema


def _assert_no_none_values(node):
if isinstance(node, dict):
for key, value in node.items():
assert value is not None, f"unexpected None at key {key!r}"
_assert_no_none_values(value)
elif isinstance(node, list):
for item in node:
_assert_no_none_values(item)


def test_prompt_parameter_defaults_omit_none_values():
prompt_data = PromptData(
prompt=PromptChatBlock(messages=[PromptMessage(role="user", content="{{input}}")]),
options={"model": "gpt-5-mini"},
)

serialized = serialize_remote_eval_parameters_container(
{
"grouping_prompt": {
"type": "prompt",
"description": "Grouping prompt",
"default": prompt_data,
}
}
)

default = serialized["schema"]["grouping_prompt"]["default"]
assert "tools" not in default["prompt"]
assert "name" not in default["prompt"]["messages"][0]
assert "function_call" not in default["prompt"]["messages"][0]
assert "tool_calls" not in default["prompt"]["messages"][0]
_assert_no_none_values(default)


def test_prompt_parameter_defaults_omit_none_values_from_dict():
prompt_default = {
"prompt": {
"type": "chat",
"messages": [
{
"role": "user",
"content": "{{input}}",
"name": None,
"function_call": None,
"tool_calls": None,
}
],
"tools": None,
},
"options": {"model": "gpt-5-mini"},
}

serialized = serialize_eval_parameters(
{
"grouping_prompt": {
"type": "prompt",
"description": "Grouping prompt",
"default": prompt_default,
}
}
)

default = serialized["grouping_prompt"]["default"]
assert "tools" not in default["prompt"]
assert "name" not in default["prompt"]["messages"][0]
assert "function_call" not in default["prompt"]["messages"][0]
assert "tool_calls" not in default["prompt"]["messages"][0]
_assert_no_none_values(default)
Loading