1616 SavedFunctionId ,
1717 ToolFunctionDefinition ,
1818)
19+ from .parameters import EvalParameters , _pydantic_to_json_schema
1920from .util import eprint
2021
2122
@@ -34,6 +35,7 @@ class _GlobalState:
3435 def __init__ (self ):
3536 self .functions : list [CodeFunction ] = []
3637 self .prompts : list [CodePrompt ] = []
38+ self .parameters : list [CodeParameters ] = []
3739
3840
3941global_ = _GlobalState ()
@@ -287,6 +289,161 @@ def create(
287289 return p
288290
289291
292+ def _maybe_serialize_prompt_default (default : Any ) -> Any :
293+ as_dict = getattr (default , "as_dict" , None )
294+ if callable (as_dict ):
295+ return as_dict ()
296+ return default
297+
298+
299+ def _pydantic_instance_to_plain (value : Any ) -> Any :
300+ if hasattr (value , "model_dump" ):
301+ return value .model_dump ()
302+ if hasattr (value , "dict" ):
303+ return value .dict ()
304+ return value
305+
306+
307+ def _is_single_field_value_model (model : Any ) -> bool :
308+ fields = getattr (model , "__fields__" , None ) or getattr (model , "model_fields" , {})
309+ return isinstance (fields , dict ) and len (fields ) == 1 and "value" in fields
310+
311+
312+ def _maybe_set_default_from_pydantic_model (model : Any , schema_obj : dict [str , Any ]) -> dict [str , Any ]:
313+ if "default" in schema_obj :
314+ return schema_obj
315+ try :
316+ instance = model ()
317+ except Exception :
318+ return schema_obj
319+
320+ if _is_single_field_value_model (model ) and hasattr (instance , "value" ):
321+ return {** schema_obj , "default" : _pydantic_instance_to_plain (getattr (instance , "value" ))}
322+
323+ return {** schema_obj , "default" : _pydantic_instance_to_plain (instance )}
324+
325+
326+ def serialize_eval_parameters_to_parameters_schema (parameters : EvalParameters ) -> dict [str , Any ]:
327+ properties : dict [str , Any ] = {}
328+ required : list [str ] = []
329+
330+ for name , schema in parameters .items ():
331+ if isinstance (schema , dict ) and schema .get ("type" ) == "prompt" :
332+ prompt_schema : dict [str , Any ] = {"type" : "object" , "x-bt-type" : "prompt" }
333+
334+ description = schema .get ("description" )
335+ if description is not None :
336+ prompt_schema ["description" ] = description
337+
338+ default_value = schema .get ("default" )
339+ if default_value is not None :
340+ prompt_schema ["default" ] = _maybe_serialize_prompt_default (default_value )
341+ else :
342+ required .append (name )
343+
344+ properties [name ] = prompt_schema
345+ continue
346+
347+ if schema is None :
348+ raise ValueError (f"Parameter '{ name } ' has no schema" )
349+
350+ if not (hasattr (schema , "model_json_schema" ) or hasattr (schema , "schema" )):
351+ raise ValueError (
352+ f"Invalid schema for parameter '{ name } '. Expected a pydantic model (v1 or v2) or a prompt parameter."
353+ )
354+
355+ schema_obj = _pydantic_to_json_schema (schema )
356+ if _is_single_field_value_model (schema ):
357+ value_schema = schema_obj .get ("properties" , {}).get ("value" )
358+ if not isinstance (value_schema , dict ):
359+ raise ValueError (f"Invalid pydantic schema for parameter '{ name } ': missing properties.value" )
360+ parameter_schema = _maybe_set_default_from_pydantic_model (schema , value_schema )
361+ else :
362+ parameter_schema = _maybe_set_default_from_pydantic_model (schema , schema_obj )
363+
364+ properties [name ] = parameter_schema
365+ if "default" not in parameter_schema :
366+ required .append (name )
367+
368+ out : dict [str , Any ] = {"type" : "object" , "properties" : properties , "additionalProperties" : True }
369+ if required :
370+ out ["required" ] = required
371+ return out
372+
373+
374+ def get_default_data_from_parameters_schema (schema : dict [str , Any ]) -> dict [str , Any ]:
375+ properties = schema .get ("properties" )
376+ if not isinstance (properties , dict ):
377+ return {}
378+
379+ return {k : v ["default" ] for k , v in properties .items () if isinstance (v , dict ) and "default" in v }
380+
381+
382+ @dataclasses .dataclass
383+ class CodeParameters :
384+ """Parameters defined in code, with metadata."""
385+
386+ project : "Project"
387+ name : str
388+ slug : str
389+ description : str | None
390+ schema : EvalParameters
391+ if_exists : IfExists | None
392+ metadata : dict [str , Any ] | None = None
393+
394+ def to_function_definition (self , if_exists : IfExists | None , project_ids : ProjectIdCache ) -> dict [str , Any ]:
395+ schema = serialize_eval_parameters_to_parameters_schema (self .schema )
396+ j : dict [str , Any ] = {
397+ "project_id" : project_ids .get (self .project ),
398+ "name" : self .name ,
399+ "slug" : self .slug ,
400+ "function_type" : "parameters" ,
401+ "function_data" : {
402+ "type" : "parameters" ,
403+ "data" : get_default_data_from_parameters_schema (schema ),
404+ "__schema" : schema ,
405+ },
406+ "if_exists" : self .if_exists if self .if_exists is not None else if_exists ,
407+ }
408+ if self .description is not None :
409+ j ["description" ] = self .description
410+ if self .metadata is not None :
411+ j ["metadata" ] = self .metadata
412+ return j
413+
414+
415+ class ParametersBuilder :
416+ """Builder to create parameters in Braintrust."""
417+
418+ def __init__ (self , project : "Project" ):
419+ self .project = project
420+
421+ def create (
422+ self ,
423+ * ,
424+ name : str ,
425+ slug : str | None = None ,
426+ description : str | None = None ,
427+ schema : EvalParameters ,
428+ if_exists : IfExists | None = None ,
429+ metadata : dict [str , Any ] | None = None ,
430+ ) -> EvalParameters :
431+ if slug is None or len (slug ) == 0 :
432+ slug = slugify .slugify (name )
433+
434+ parameters = CodeParameters (
435+ project = self .project ,
436+ name = name ,
437+ slug = slug ,
438+ description = description ,
439+ schema = schema ,
440+ if_exists = if_exists ,
441+ metadata = metadata ,
442+ )
443+ self .project .add_parameters (parameters )
444+ return schema
445+
446+
290447class ScorerBuilder :
291448 """Builder to create a scorer in Braintrust."""
292449
@@ -461,10 +618,12 @@ def __init__(self, name: str):
461618 self .name = name
462619 self .tools = ToolBuilder (self )
463620 self .prompts = PromptBuilder (self )
621+ self .parameters = ParametersBuilder (self )
464622 self .scorers = ScorerBuilder (self )
465623
466624 self ._publishable_code_functions : list [CodeFunction ] = []
467625 self ._publishable_prompts : list [CodePrompt ] = []
626+ self ._publishable_parameters : list [CodeParameters ] = []
468627
469628 def add_code_function (self , fn : CodeFunction ):
470629 self ._publishable_code_functions .append (fn )
@@ -476,6 +635,11 @@ def add_prompt(self, prompt: CodePrompt):
476635 if _is_lazy_load ():
477636 global_ .prompts .append (prompt )
478637
638+ def add_parameters (self , parameters : CodeParameters ):
639+ self ._publishable_parameters .append (parameters )
640+ if _is_lazy_load ():
641+ global_ .parameters .append (parameters )
642+
479643 def publish (self ):
480644 if _is_lazy_load ():
481645 eprint (f"{ bcolors .WARNING } publish() is a no-op when running `braintrust push`.{ bcolors .ENDC } " )
0 commit comments