From b6bcc7816e62169b80f97e20d1489c203db04265 Mon Sep 17 00:00:00 2001 From: John Smith Date: Thu, 31 Oct 2024 21:19:20 +1030 Subject: [PATCH] feat(go): Run polling --- .github/workflows/build.yml | 7 +- sdk-dotnet/README.md | 12 +- sdk-dotnet/src/API/Models.cs | 14 +- .../tests/Inferable.Tests/InferableTest.cs | 2 +- sdk-go/README.md | 62 +++--- sdk-go/inferable.go | 188 ++++++++---------- sdk-go/internal/client/client.go | 23 --- sdk-go/internal/util/test_util.go | 20 +- sdk-go/main_test.go | 76 +++++++ sdk-go/service.go | 10 +- sdk-node/README.md | 8 +- 11 files changed, 229 insertions(+), 193 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f5f9b4f6..cd193ff2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -177,7 +177,6 @@ jobs: - name: Test run: go test -v ./... env: - INFERABLE_API_ENDPOINT: "https://api.inferable.ai" - INFERABLE_CLUSTER_ID: ${{ secrets.INFERABLE_CLUSTER_ID }} - INFERABLE_MACHINE_SECRET: ${{ secrets.INFERABLE_MACHINE_SECRET }} - INFERABLE_CONSUME_SECRET: ${{ secrets.INFERABLE_CONSUME_SECRET }} + INFERABLE_TEST_API_ENDPOINT: "https://api.inferable.ai" + INFERABLE_TEST_CLUSTER_ID: ${{ secrets.INFERABLE_CLUSTER_ID }} + INFERABLE_TEST_API_SECRET: ${{ secrets.INFERABLE_MACHINE_SECRET }} diff --git a/sdk-dotnet/README.md b/sdk-dotnet/README.md index 49a686d8..7e9a613a 100644 --- a/sdk-dotnet/README.md +++ b/sdk-dotnet/README.md @@ -63,7 +63,15 @@ client.Default.RegisterFunction(new FunctionRegistration await client.Default.Start(); ``` -### 3. Trigger a run +
+ +👉 The DotNet SDK for Inferable reflects the types from the input class of the function. + +Unlike the [NodeJs SDK](https://github.com/inferablehq/inferable/sdk-node), the Dotnet SDK for Inferable reflects the types from the input struct of the function. It uses the [NJsonSchema](https://github.com/RicoSuter/NJsonSchema) under the hood to generate JSON schemas from C# types through reflection. + +
+ +### Triggering a run The following code will create an [Inferable run](https://docs.inferable.ai/pages/runs) with the prompt "Say hello to John" and the `sayHello` function attached. @@ -87,7 +95,7 @@ var run = await inferable.CreateRun(new CreateRunInput // Optional: Define a schema for the result to conform to ResultSchema = JsonSchema.FromType(); // Optional: Subscribe an Inferable function to receive notifications when the run status changes - //OnStatusChange = new CreateOnStatusChangeInput + //OnStatusChange = new OnStatusChange //{ // Function = OnStatusChangeFunction //} diff --git a/sdk-dotnet/src/API/Models.cs b/sdk-dotnet/src/API/Models.cs index ae23d06b..87db3bf8 100644 --- a/sdk-dotnet/src/API/Models.cs +++ b/sdk-dotnet/src/API/Models.cs @@ -144,21 +144,21 @@ public struct CreateRunInput ] public Dictionary? Metadata { get; set; } - [ - JsonPropertyName("onStatusChange"), - JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull) - ] - public CreateOnStatusChangeInput? OnStatusChange { get; set; } - [ JsonPropertyName("resultSchema"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull), JsonConverter(typeof(JsonSchemaConverter)) ] public JsonSchema? ResultSchema { get; set; } + + [ + JsonPropertyName("onStatusChange"), + JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull) + ] + public OnStatusChange? OnStatusChange { get; set; } } - public struct CreateOnStatusChangeInput + public struct OnStatusChange { [ JsonPropertyName("function"), diff --git a/sdk-dotnet/tests/Inferable.Tests/InferableTest.cs b/sdk-dotnet/tests/Inferable.Tests/InferableTest.cs index e772c21f..b2f63aa4 100644 --- a/sdk-dotnet/tests/Inferable.Tests/InferableTest.cs +++ b/sdk-dotnet/tests/Inferable.Tests/InferableTest.cs @@ -269,7 +269,7 @@ async public void Inferable_Run_E2E() { SayHelloFunction }, - OnStatusChange = new CreateOnStatusChangeInput + OnStatusChange = new OnStatusChange { Function = OnStatusChangeFunction }, diff --git a/sdk-go/README.md b/sdk-go/README.md index c84ee8bd..16977b1d 100644 --- a/sdk-go/README.md +++ b/sdk-go/README.md @@ -35,9 +35,12 @@ if err != nil { } ``` -If you don't provide an API endpoint, it will use the default endpoint: `https://api.inferable.ai`. +If you don't provide an API key or base URL, it will attempt to read them from the following environment variables: -### Hello World Function +- `INFERABLE_API_SECRET` +- `INFERABLE_API_ENDPOINT` + +### Registering a Function Register a "SayHello" [function](https://docs.inferable.ai/pages/functions) with the [control-plane](https://docs.inferable.ai/pages/control-plane). @@ -61,7 +64,7 @@ if err != nil { 👉 The Golang SDK for Inferable reflects the types from the input struct of the function. -Unlike the TypeScript schema, the Golang SDK for Inferable reflects the types from the input struct of the function. It uses the [invopop/jsonschema](https://pkg.go.dev/github.com/invopop/jsonschema) library under the hood to generate JSON schemas from Go types through reflection. +Unlike the [NodeJs SDK](https://github.com/inferablehq/inferable/sdk-node), the Golang SDK for Inferable reflects the types from the input struct of the function. It uses the [invopop/jsonschema](https://pkg.go.dev/github.com/invopop/jsonschema) library under the hood to generate JSON schemas from Go types through reflection. If the input struct defines jsonschema properties using struct tags, the SDK will use those in the generated schema. This allows for fine-grained control over the schema generation. @@ -112,26 +115,7 @@ The [invopop/jsonschema library](https://pkg.go.dev/github.com/invopop/jsonschem -### Starting the Service - -To start the service and begin listening for incoming requests: - -```go -err := service.Start() -if err != nil { - // Handle error -} -``` - -### Stopping the Service - -To stop the service: - -```go -service.Stop() -``` - -### Trigger a run +### Triggering a run The following code will create an [Inferable run](https://docs.inferable.ai/pages/runs) with the prompt "Say hello to John" and the `sayHello` function attached. @@ -141,23 +125,39 @@ The following code will create an [Inferable run](https://docs.inferable.ai/page > - in the [CLI](https://www.npmjs.com/package/@inferable/cli) via `inf runs list` ```typescript - run, err := i.CreateRun(&inferable.Run{ + run, err := i.CreateRun(inferable.CreateRunInput{ Message: "Say hello to John Smith", - Functions: []*inferable.FunctionHandle{ - sayHello, + AttachedFunctions: []*inferable.FunctionReference{ + inferable.FunctionReference{ + Function: "SayHello", + Service: "default", + } }, - // Optionally, subscribe an Inferable function as a result handler which will be called when the run is complete. - // Result: &inferable.RunResult{Handler: resultHandler}, + // Optional: Subscribe an Inferable function to receive notifications when the run status changes + //OnStatusChange: &inferable.OnStatusChange{ + // Function: OnStatusChangeFunction + //} }) + fmt.Println("Run started: ", run.ID) + result, err := run.Poll(nil) + if err != nil { + panic(err) + } + fmt.Println("Run Result: ", result) + ``` > Runs can also be triggered via the [API](https://docs.inferable.ai/pages/invoking-a-run-api), [CLI](https://www.npmjs.com/package/@inferable/cli) or [playground UI](https://app.inferable.ai/). -## Contributing +## Documentation -Contributions to the Inferable Go Client are welcome. Please ensure that your code adheres to the existing style and includes appropriate tests. +- [Inferable documentation](https://docs.inferable.ai/) contains all the information you need to get started with Inferable. ## Support -For support or questions, please [create an issue in the repository](https://github.com/inferablehq/inferable/sdk-go/issues). +For support or questions, please [create an issue in the repository](https://github.com/inferablehq/inferable/issues) or [join the Discord](https://discord.gg/WHcTNeDP) + +## Contributing + +Contributions to the Inferable Go Client are welcome. Please ensure that your code adheres to the existing style and includes appropriate tests. diff --git a/sdk-go/inferable.go b/sdk-go/inferable.go index 667c7da0..2ee4d8c5 100644 --- a/sdk-go/inferable.go +++ b/sdk-go/inferable.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "reflect" + "time" "github.com/inferablehq/inferable/sdk-go/internal/client" "github.com/inferablehq/inferable/sdk-go/internal/util" @@ -39,8 +40,8 @@ type InferableOptions struct { ClusterID string } -// Input struct passed to a Run's result handler -type RunResultHandlerInput struct { +// Struct type that will be returned to a Run's OnStatusChange Function +type OnStatusChangeInput struct { Status string `json:"status"` RunId string `json:"runId"` Result interface{} `json:"result"` @@ -48,31 +49,34 @@ type RunResultHandlerInput struct { Metadata interface{} `json:"metadata"` } -type RunResult struct { - Handler *FunctionHandle - Schema interface{} -} +type runResult = OnStatusChangeInput type RunTemplate struct { - ID string - Input map[string]interface{} + ID string `json:"id"` + Input map[string]interface{} `json:"input"` +} + +type OnStatusChange struct { + Function *FunctionReference `json:"function"` } -type Run struct { - Functions []*FunctionHandle - Message string - Result *RunResult - Metadata map[string]string - Template *RunTemplate +type CreateRunInput struct { + AttachedFunctions []*FunctionReference `json:"attachedFunctions,omitempty"` + Message string `json:"message"` + OnStatusChange *OnStatusChange `json:"onStatusChange,omitempty"` + ResultSchema interface{} `json:"resultSchema,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` + Template *RunTemplate `json:"template,omitempty"` } -type runHandle struct { - ID string +type PollOptions struct { + MaxWaitTime *time.Duration + Interval *time.Duration } -type templateHandle struct { - ID string - Run func(input *Run) (*runHandle, error) +type runReference struct { + ID string + Poll func(options *PollOptions) (*runResult, error) } func New(options InferableOptions) (*Inferable, error) { @@ -123,44 +127,44 @@ func (i *Inferable) RegisterService(serviceName string) (*service, error) { return service, nil } -func (i *Inferable) CreateRun(input *Run) (*runHandle, error) { +func (i *Inferable) getRun(runID string) (*runResult, error) { + if i.clusterID == "" { + return nil, fmt.Errorf("Cluster ID must be provided to manage runs") + } + + // Prepare headers + headers := map[string]string{ + "Authorization": "Bearer " + i.apiSecret, + "X-Machine-ID": i.machineID, + "X-Machine-SDK-Version": Version, + "X-Machine-SDK-Language": "go", + } + + options := client.FetchDataOptions{ + Path: fmt.Sprintf("/clusters/%s/runs/%s", i.clusterID, runID), + Method: "GET", + Headers: headers, + } + + responseData, _, err, _ := i.fetchData(options) + if err != nil { + return nil, fmt.Errorf("failed to get run: %v", err) + } + var result runResult + err = json.Unmarshal(responseData, &result) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %v", err) + } + return &result, nil +} + +func (i *Inferable) CreateRun(input CreateRunInput) (*runReference, error) { if i.clusterID == "" { return nil, fmt.Errorf("cluster ID must be provided to manage runs") } - var attachedFunctions []string - for _, fn := range input.Functions { - attachedFunctions = append(attachedFunctions, fmt.Sprintf("%s_%s", fn.Service, fn.Function)) - } - - payload := client.CreateRunInput{ - Message: input.Message, - AttachedFunctions: attachedFunctions, - Metadata: input.Metadata, - } - - if input.Template != nil { - payload.Template = &client.CreateRunTemplateInput{ - Input: input.Template.Input, - ID: input.Template.ID, - } - } - - if input.Result != nil { - payload.Result = &client.CreateRunResultInput{} - if input.Result.Handler != nil { - payload.Result.Handler = &client.CreateRunResultHandlerInput{ - Service: input.Result.Handler.Service, - Function: input.Result.Handler.Function, - } - } - if input.Result.Schema != nil { - payload.Result.Schema = input.Result.Schema - } - } - // Marshal the payload to JSON - jsonPayload, err := json.Marshal(payload) + jsonPayload, err := json.Marshal(input) if err != nil { return nil, fmt.Errorf("failed to marshal payload: %v", err) } @@ -196,64 +200,42 @@ func (i *Inferable) CreateRun(input *Run) (*runHandle, error) { return nil, fmt.Errorf("failed to parse run response: %v", err) } - return &runHandle{ID: response.ID}, nil -} + return &runReference{ + ID: response.ID, + Poll: func(options *PollOptions) (*runResult, error) { + // Default values for polling options + maxWaitTime := 60 * time.Second + interval := 500 * time.Millisecond -func (i *Inferable) GetTemplate(id string) (*templateHandle, error) { - if i.clusterID == "" { - return nil, fmt.Errorf("cluster ID must be provided to manage runs") - } + if options != nil { + if options.MaxWaitTime != nil { + maxWaitTime = *options.MaxWaitTime + } - // Prepare headers - headers := map[string]string{ - "Authorization": "Bearer " + i.apiSecret, - "X-Machine-ID": i.machineID, - "X-Machine-SDK-Version": Version, - "X-Machine-SDK-Language": "go", - } - - // Call the registerMachine endpoint - options := client.FetchDataOptions{ - Path: fmt.Sprintf("/clusters/%s/prompt-templates/%s", i.clusterID, id), - Method: "GET", - Headers: headers, - } - - responseData, _, err, _ := i.fetchData(options) - if err != nil { - return nil, fmt.Errorf("failed to get template: %v", err) - } + if options.Interval != nil { + interval = *options.Interval + } + } - // Parse the response - var response struct { - ID string `json:"id"` - } + start := time.Now() + end := start.Add(maxWaitTime) - err = json.Unmarshal(responseData, &response) - if err != nil { - return nil, fmt.Errorf("failed to parse template response: %v", err) - } + for time.Now().Before(end) { + pollResult, err := i.getRun(response.ID) + if err != nil { + return nil, fmt.Errorf("failed to poll for run: %w", err) + } - return &templateHandle{ - ID: response.ID, - Run: func(input *Run) (*runHandle, error) { - // CLone the input - inputCopy := *input - - // Set the template ID - if inputCopy.Template == nil { - inputCopy.Template = &RunTemplate{ - ID: response.ID, - } - } else { - inputCopy.Template.ID = response.ID - } + if pollResult.Status != "pending" && pollResult.Status != "running" { + return pollResult, nil + } - fmt.Println(inputCopy) + time.Sleep(interval) + } - return i.CreateRun(&inputCopy) - }, - }, nil + return nil, fmt.Errorf("max wait time reached, polling stopped") + }, + }, nil } func (i *Inferable) callFunc(serviceName, funcName string, args ...interface{}) ([]reflect.Value, error) { diff --git a/sdk-go/internal/client/client.go b/sdk-go/internal/client/client.go index b47ea5ef..f096a5bf 100644 --- a/sdk-go/internal/client/client.go +++ b/sdk-go/internal/client/client.go @@ -32,29 +32,6 @@ func NewClient(options ClientOptions) (*Client, error) { }, nil } -type CreateRunInput struct { - Message string `json:"message,omitempty"` - AttachedFunctions []string`json:"attachedFunctions,omitempty"` - Metadata map[string]string `json:"metadata,omitempty"` - Result *CreateRunResultInput `json:"result,omitempty"` - Template *CreateRunTemplateInput `json:"template,omitempty"` -} - -type CreateRunTemplateInput struct { - Input map[string]interface{} `json:"input,omitempty"` - ID string `json:"id,omitempty"` -} - -type CreateRunResultInput struct { - Handler *CreateRunResultHandlerInput `json:"handler,omitempty"` - Schema interface{} `json:"schema,omitempty"` -} - -type CreateRunResultHandlerInput struct { - Service string `json:"service"` - Function string `json:"function"` -} - type FetchDataOptions struct { Path string Headers map[string]string diff --git a/sdk-go/internal/util/test_util.go b/sdk-go/internal/util/test_util.go index dc278daf..ac3bf5d2 100644 --- a/sdk-go/internal/util/test_util.go +++ b/sdk-go/internal/util/test_util.go @@ -6,29 +6,25 @@ import ( ) func GetTestVars() (string, string, string, string) { - if os.Getenv("INFERABLE_MACHINE_SECRET") == "" { + if os.Getenv("INFERABLE_TEST_API_ENDPOINT") == "" { err := godotenv.Load("./.env") if err != nil { panic(err) } } - machineSecret := os.Getenv("INFERABLE_MACHINE_SECRET") - consumeSecret := os.Getenv("INFERABLE_CONSUME_SECRET") - clusterId := os.Getenv("INFERABLE_CLUSTER_ID") - apiEndpoint := os.Getenv("INFERABLE_API_ENDPOINT") + machineSecret := os.Getenv("INFERABLE_TEST_API_SECRET") + clusterId := os.Getenv("INFERABLE_TEST_CLUSTER_ID") + apiEndpoint := os.Getenv("INFERABLE_TEST_API_ENDPOINT") if apiEndpoint == "" { - panic("INFERABLE_API_ENDPOINT is not available") + panic("INFERABLE_TEST_API_ENDPOINT is not available") } if machineSecret == "" { - panic("INFERABLE_MACHINE_SECRET is not available") - } - if consumeSecret == "" { - panic("INFERABLE_CONSUME_SECRET is not available") + panic("INFERABLE_TEST_API_SECRET is not available") } if clusterId == "" { - panic("INFERABLE_CLUSTER_ID is not set in .env") + panic("INFERABLE_TEST_CLUSTER_ID is not set in .env") } - return machineSecret, consumeSecret, clusterId, apiEndpoint + return machineSecret, machineSecret, clusterId, apiEndpoint } diff --git a/sdk-go/main_test.go b/sdk-go/main_test.go index 8ee4fc7a..8b96e22f 100644 --- a/sdk-go/main_test.go +++ b/sdk-go/main_test.go @@ -1,7 +1,9 @@ package inferable import ( + "fmt" "testing" + "time" "github.com/inferablehq/inferable/sdk-go/internal/util" ) @@ -10,6 +12,7 @@ type EchoInput struct { Input string } + func echo(input EchoInput) string { return input.Input } @@ -144,3 +147,76 @@ func TestInferableFunctions(t *testing.T) { } }) } + +// This should match the example in the readme +func TestInferableE2E(t *testing.T) { + machineSecret, _, clusterID, apiEndpoint := util.GetTestVars() + + client, err := New(InferableOptions{ + APIEndpoint: apiEndpoint, + APISecret: machineSecret, + ClusterID: clusterID, + }) + + if err != nil { + t.Fatalf("Error creating Inferable instance: %v", err) + } + + didCallSayHello := false + didCallResultHandler := false + + sayHello, err := client.Default.RegisterFunc(Function{ + Func: func(input EchoInput) string { + didCallSayHello = true + return "Hello " + input.Input + }, + Name: "SayHello", + Description: "A simple greeting function", + }) + + resultHandler, err := client.Default.RegisterFunc(Function{ + Func: func(input OnStatusChangeInput) string { + didCallResultHandler = true + fmt.Println("OnStatusChange: ", input) + return "" + }, + Name: "ResultHandler", + }) + + client.Default.Start() + + run, err := client.CreateRun(CreateRunInput{ + Message: "Say hello to John Smith", + AttachedFunctions: []*FunctionReference{ + sayHello, + }, + OnStatusChange: &OnStatusChange{ + Function: resultHandler, + }, + }) + + if err != nil { + panic(err) + } + + fmt.Println("Run started: ", run.ID) + result, err := run.Poll(nil) + if err != nil { + panic(err) + } + fmt.Println("Run Result: ", result) + + time.Sleep(500 * time.Millisecond) + + if result == nil { + t.Error("Result is nil") + } + + if !didCallSayHello { + t.Error("SayHello function was not called") + } + + if !didCallResultHandler { + t.Error("OnStatusChange function was not called") + } +} diff --git a/sdk-go/service.go b/sdk-go/service.go index b1699bb5..0cdf3c93 100644 --- a/sdk-go/service.go +++ b/sdk-go/service.go @@ -54,12 +54,12 @@ type callResult struct { Meta callResultMeta `json:"meta"` } -type FunctionHandle struct { - Service string - Function string +type FunctionReference struct { + Service string `json:"service"` + Function string `json:"function"` } -func (s *service) RegisterFunc(fn Function) (*FunctionHandle, error) { +func (s *service) RegisterFunc(fn Function) (*FunctionReference, error) { if s.isPolling() { return nil, fmt.Errorf("functions must be registered before starting the service.") } @@ -105,7 +105,7 @@ func (s *service) RegisterFunc(fn Function) (*FunctionHandle, error) { fn.schema = defs s.Functions[fn.Name] = fn - return &FunctionHandle{Service: s.Name, Function: fn.Name}, nil + return &FunctionReference{Service: s.Name, Function: fn.Name}, nil } func (s *service) registerMachine() error { diff --git a/sdk-node/README.md b/sdk-node/README.md index 41dd9e88..e680658d 100644 --- a/sdk-node/README.md +++ b/sdk-node/README.md @@ -33,11 +33,9 @@ pnpm add inferable ## Quick Start -### 1. Initializing Inferable +### Initializing the Client ```typescript -// d.ts - import { Inferable } from "inferable"; // Initialize the Inferable client with your API secret. @@ -52,7 +50,7 @@ If you don't provide an API key or base URL, it will attempt to read them from t - `INFERABLE_API_SECRET` - `INFERABLE_API_ENDPOINT` -### 2. Hello World Function +### Registering a Function Register a "sayHello" [function](https://docs.inferable.ai/pages/functions). This file will register the function with the [control-plane](https://docs.inferable.ai/pages/control-plane). @@ -74,7 +72,7 @@ const sayHello = client.default.register({ client.default.start(); ``` -### 3. Trigger a run +### Triggering a run The following code will create an [Inferable run](https://docs.inferable.ai/pages/runs) with the prompt "Say hello to John" and the `sayHello` function attached.