@@ -66,6 +66,26 @@ Optional<Prompt> getPrompt(
6666 /** Query datasets by project name and dataset name */
6767 List <Dataset > queryDatasets (String projectName , String datasetName );
6868
69+ /**
70+ * Get a function by project name and slug, with optional version.
71+ *
72+ * @param projectName the name of the project containing the function
73+ * @param slug the unique slug identifier for the function
74+ * @param version optional version identifier (transaction id or version string)
75+ * @return the function if found
76+ */
77+ Optional <Function > getFunction (
78+ @ Nonnull String projectName , @ Nonnull String slug , @ Nullable String version );
79+
80+ /**
81+ * Invoke a function (scorer, prompt, or tool) by its ID.
82+ *
83+ * @param functionId the ID of the function to invoke
84+ * @param request the invocation request containing input, expected output, etc.
85+ * @return the result of the function invocation
86+ */
87+ Object invokeFunction (@ Nonnull String functionId , @ Nonnull FunctionInvokeRequest request );
88+
6989 static BraintrustApiClient of (BraintrustConfig config ) {
7090 return new HttpImpl (config );
7191 }
@@ -296,6 +316,54 @@ public List<Dataset> queryDatasets(String projectName, String datasetName) {
296316 }
297317 }
298318
319+ @ Override
320+ public Optional <Function > getFunction (
321+ @ Nonnull String projectName , @ Nonnull String slug , @ Nullable String version ) {
322+ Objects .requireNonNull (projectName , "projectName must not be null" );
323+ Objects .requireNonNull (slug , "slug must not be null" );
324+ try {
325+ var uriBuilder = new StringBuilder ("/v1/function?" );
326+ uriBuilder .append ("slug=" ).append (slug );
327+ uriBuilder .append ("&project_name=" ).append (projectName );
328+
329+ if (version != null && !version .isEmpty ()) {
330+ uriBuilder .append ("&version=" ).append (version );
331+ }
332+
333+ FunctionListResponse response =
334+ getAsync (uriBuilder .toString (), FunctionListResponse .class ).get ();
335+
336+ if (response .objects () == null || response .objects ().isEmpty ()) {
337+ return Optional .empty ();
338+ }
339+
340+ if (response .objects ().size () > 1 ) {
341+ throw new ApiException (
342+ "Multiple functions found for slug: "
343+ + slug
344+ + ", projectName: "
345+ + projectName );
346+ }
347+
348+ return Optional .of (response .objects ().get (0 ));
349+ } catch (InterruptedException | ExecutionException e ) {
350+ throw new RuntimeException (e );
351+ }
352+ }
353+
354+ @ Override
355+ public Object invokeFunction (
356+ @ Nonnull String functionId , @ Nonnull FunctionInvokeRequest request ) {
357+ Objects .requireNonNull (functionId , "functionId must not be null" );
358+ Objects .requireNonNull (request , "request must not be null" );
359+ try {
360+ String path = "/v1/function/" + functionId + "/invoke" ;
361+ return postAsync (path , request , Object .class ).get ();
362+ } catch (InterruptedException | ExecutionException e ) {
363+ throw new ApiException ("Failed to invoke function: " + functionId , e );
364+ }
365+ }
366+
299367 private <T > CompletableFuture <T > getAsync (String path , Class <T > responseType ) {
300368 var request =
301369 HttpRequest .newBuilder ()
@@ -399,6 +467,9 @@ class InMemoryImpl implements BraintrustApiClient {
399467 private final Set <Experiment > experiments =
400468 Collections .newSetFromMap (new ConcurrentHashMap <>());
401469 private final List <Prompt > prompts = new ArrayList <>();
470+ private final List <Function > functions = new ArrayList <>();
471+ private final Map <String , java .util .function .Function <FunctionInvokeRequest , Object >>
472+ functionInvokers = new ConcurrentHashMap <>();
402473
403474 public InMemoryImpl (OrganizationAndProjectInfo ... organizationAndProjectInfos ) {
404475 this .organizationAndProjectInfos =
@@ -583,6 +654,17 @@ public Optional<Dataset> getDataset(String datasetId) {
583654 public List <Dataset > queryDatasets (String projectName , String datasetName ) {
584655 return List .of ();
585656 }
657+
658+ @ Override
659+ public Optional <Function > getFunction (@ Nonnull String projectName , @ Nonnull String slug , @ Nullable String version ) {
660+ throw new RuntimeException ("will not be invoked" );
661+ }
662+
663+ @ Override
664+ public Object invokeFunction (
665+ @ Nonnull String functionId , @ Nonnull FunctionInvokeRequest request ) {
666+ throw new RuntimeException ("will not be invoked" );
667+ }
586668 }
587669
588670 // Request/Response DTOs
@@ -681,4 +763,59 @@ record Prompt(
681763 Optional <Object > metadata ) {}
682764
683765 record PromptListResponse (List <Prompt > objects ) {}
766+
767+ // Function models for remote scorers/prompts/tools
768+
769+ /**
770+ * Represents a Braintrust function (scorer, prompt, tool, or task). Functions can be invoked
771+ * remotely via the API.
772+ */
773+ record Function (
774+ String id ,
775+ String projectId ,
776+ String orgId ,
777+ String name ,
778+ String slug ,
779+ Optional <String > description ,
780+ String created ,
781+ Optional <Object > functionData ,
782+ Optional <Object > promptData ,
783+ Optional <List <String >> tags ,
784+ Optional <Object > metadata ,
785+ Optional <String > functionType ,
786+ Optional <Object > origin ,
787+ Optional <Object > functionSchema ) {}
788+
789+ record FunctionListResponse (List <Function > objects ) {}
790+
791+ /**
792+ * Request body for invoking a function. The input field wraps the function arguments.
793+ *
794+ * <p>For remote Python/TypeScript scorers, the scorer handler parameters (input, output,
795+ * expected, metadata) must be wrapped in the outer input field.
796+ */
797+ record FunctionInvokeRequest (@ Nullable Object input ) {
798+
799+ /** Create a simple invoke request with just input */
800+ public static FunctionInvokeRequest of (Object input ) {
801+ return new FunctionInvokeRequest (input );
802+ }
803+
804+ /**
805+ * Create an invoke request for a scorer with input, output, expected, and metadata. This
806+ * maps to the standard scorer handler signature: handler(input, output, expected, metadata)
807+ *
808+ * <p>The scorer args are wrapped in the outer input field as required by the invoke API.
809+ */
810+ public static FunctionInvokeRequest forScorer (
811+ Object input , Object output , Object expected , Object metadata ) {
812+ // Wrap scorer args in an inner map that becomes the outer "input" field
813+ var scorerArgs = new java .util .LinkedHashMap <String , Object >();
814+ scorerArgs .put ("input" , input );
815+ scorerArgs .put ("output" , output );
816+ scorerArgs .put ("expected" , expected );
817+ scorerArgs .put ("metadata" , metadata );
818+ return new FunctionInvokeRequest (scorerArgs );
819+ }
820+ }
684821}
0 commit comments