Skip to content

Commit 596586c

Browse files
committed
Handle object default value parsing
1 parent b7fbdfb commit 596586c

1 file changed

Lines changed: 55 additions & 9 deletions

File tree

py/src/braintrust/parameters.py

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,51 @@ def validate_parameters(
157157
return result
158158

159159

160+
def _extract_pydantic_default_and_description(
161+
model: Any, schema_obj: dict[str, Any]
162+
) -> tuple[Any, str | None]:
163+
"""Extract default value and description from a pydantic model's JSON schema.
164+
165+
In the JS SDK, Zod's zodToJsonSchema() puts `default` and `description` at the
166+
top level of the generated schema (e.g. z.string().default("x").describe("y")
167+
produces {"type": "string", "default": "x", "description": "y"}). This makes
168+
it trivial to read them off the schema object (see
169+
serializeEvalParametersToStaticParametersSchema in sdk/js/src/framework2.ts).
170+
171+
Pydantic works differently. For the single-field wrapper pattern we use
172+
(class Param(BaseModel): value: T = default), the default and description end up
173+
nested inside properties.value rather than at the top level. For multi-field models,
174+
pydantic doesn't emit a top-level default at all — we have to instantiate the model
175+
to extract it.
176+
"""
177+
fields = getattr(model, "__fields__", None) or getattr(model, "model_fields", {})
178+
is_single_value = isinstance(fields, dict) and len(fields) == 1 and "value" in fields
179+
180+
if is_single_value:
181+
value_schema = schema_obj.get("properties", {}).get("value", {})
182+
default = value_schema.get("default")
183+
description = value_schema.get("description")
184+
if default is None:
185+
try:
186+
instance = model()
187+
raw = getattr(instance, "value")
188+
default = raw.model_dump() if hasattr(raw, "model_dump") else (raw.dict() if hasattr(raw, "dict") else raw)
189+
except Exception:
190+
pass
191+
return default, description
192+
193+
default = schema_obj.get("default")
194+
description = schema_obj.get("description")
195+
if default is None:
196+
try:
197+
instance = model()
198+
default = instance.model_dump() if hasattr(instance, "model_dump") else instance.dict()
199+
except Exception:
200+
pass
201+
202+
return default, description
203+
204+
160205
def parameters_to_json_schema(parameters: EvalParameters | RemoteEvalParameters | dict | None) -> dict[str, Any]:
161206
"""
162207
Convert EvalParameters to JSON schema format for serialization.
@@ -198,22 +243,23 @@ def parameters_to_json_schema(parameters: EvalParameters | RemoteEvalParameters
198243

199244
for name, schema in parameters.items():
200245
if isinstance(schema, dict) and schema.get("type") == "prompt":
201-
# Prompt parameter
202246
result[name] = {
203247
"type": "prompt",
204248
"default": schema.get("default"),
205249
"description": schema.get("description"),
206250
}
207251
else:
208-
# Pydantic model
209252
try:
210-
result[name] = {
211-
"type": "data",
212-
"schema": _pydantic_to_json_schema(schema),
213-
# TODO: Extract default and description from pydantic model
214-
}
253+
schema_obj = _pydantic_to_json_schema(schema)
215254
except ValueError:
216-
# Not a pydantic model, skip
217-
pass
255+
continue
256+
257+
default, description = _extract_pydantic_default_and_description(schema, schema_obj)
258+
entry: dict[str, Any] = {"type": "data", "schema": schema_obj}
259+
if default is not None:
260+
entry["default"] = default
261+
if description is not None:
262+
entry["description"] = description
263+
result[name] = entry
218264

219265
return result

0 commit comments

Comments
 (0)