@@ -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,18 @@ 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 (
660+ @ Nonnull String projectName , @ Nonnull String slug , @ Nullable String version ) {
661+ throw new RuntimeException ("will not be invoked" );
662+ }
663+
664+ @ Override
665+ public Object invokeFunction (
666+ @ Nonnull String functionId , @ Nonnull FunctionInvokeRequest request ) {
667+ throw new RuntimeException ("will not be invoked" );
668+ }
586669 }
587670
588671 // Request/Response DTOs
@@ -681,4 +764,59 @@ record Prompt(
681764 Optional <Object > metadata ) {}
682765
683766 record PromptListResponse (List <Prompt > objects ) {}
767+
768+ // Function models for remote scorers/prompts/tools
769+
770+ /**
771+ * Represents a Braintrust function (scorer, prompt, tool, or task). Functions can be invoked
772+ * remotely via the API.
773+ */
774+ record Function (
775+ String id ,
776+ String projectId ,
777+ String orgId ,
778+ String name ,
779+ String slug ,
780+ Optional <String > description ,
781+ String created ,
782+ Optional <Object > functionData ,
783+ Optional <Object > promptData ,
784+ Optional <List <String >> tags ,
785+ Optional <Object > metadata ,
786+ Optional <String > functionType ,
787+ Optional <Object > origin ,
788+ Optional <Object > functionSchema ) {}
789+
790+ record FunctionListResponse (List <Function > objects ) {}
791+
792+ /**
793+ * Request body for invoking a function. The input field wraps the function arguments.
794+ *
795+ * <p>For remote Python/TypeScript scorers, the scorer handler parameters (input, output,
796+ * expected, metadata) must be wrapped in the outer input field.
797+ */
798+ record FunctionInvokeRequest (@ Nullable Object input ) {
799+
800+ /** Create a simple invoke request with just input */
801+ public static FunctionInvokeRequest of (Object input ) {
802+ return new FunctionInvokeRequest (input );
803+ }
804+
805+ /**
806+ * Create an invoke request for a scorer with input, output, expected, and metadata. This
807+ * maps to the standard scorer handler signature: handler(input, output, expected, metadata)
808+ *
809+ * <p>The scorer args are wrapped in the outer input field as required by the invoke API.
810+ */
811+ public static FunctionInvokeRequest forScorer (
812+ Object input , Object output , Object expected , Object metadata ) {
813+ // Wrap scorer args in an inner map that becomes the outer "input" field
814+ var scorerArgs = new java .util .LinkedHashMap <String , Object >();
815+ scorerArgs .put ("input" , input );
816+ scorerArgs .put ("output" , output );
817+ scorerArgs .put ("expected" , expected );
818+ scorerArgs .put ("metadata" , metadata );
819+ return new FunctionInvokeRequest (scorerArgs );
820+ }
821+ }
684822}
0 commit comments