From e97f0be2717d01511f1c3f02e8d689ba0defad26 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 28 Oct 2024 15:01:36 +1030 Subject: [PATCH 1/4] feat: Add run creation to .NET SDK --- .github/workflows/build.yml | 7 +- sdk-dotnet/README.md | 38 ++++++--- sdk-dotnet/src/API/APIClient.cs | 74 ++++++++++++++++- sdk-dotnet/src/API/Models.cs | 71 +++++++++++++++++ sdk-dotnet/src/Inferable.cs | 79 +++++++++++++++++-- .../tests/Inferable.Tests/InferableTest.cs | 73 ++++++++++++++++- 6 files changed, 312 insertions(+), 30 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 936ee20d..2b3e3c11 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -132,10 +132,9 @@ jobs: - name: Test run: dotnet test --no-restore 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 }} build-go: needs: check_changes diff --git a/sdk-dotnet/README.md b/sdk-dotnet/README.md index 1ac5dccc..da56c598 100644 --- a/sdk-dotnet/README.md +++ b/sdk-dotnet/README.md @@ -56,33 +56,45 @@ client.Default.RegisterFunction(new FunctionRegistration Function = new Func>((input) => { // Your code here }), - Name = "MyFunction", + Name = "SayHello", Description = "A simple greeting function", }); await client.Default.Start(); ``` -### Starting and Stopping a Service +### 3. Trigger a run -The example above used the Default service, you can also register separate named services. +The following code will create an [Inferable run](https://docs.inferable.ai/pages/runs) with the message "Call the testFn" and the `TestFn` function attached. + +> You can inspect the progress of the run: +> +> - in the [playground UI](https://app.inferable.ai/) via `inf app` +> - in the [CLI](https://www.npmjs.com/package/@inferable/cli) via `inf runs list` ```csharp -var userService = client.RegisterService(new ServiceRegistration +var run = await inferable.CreateRun(new CreateRunInput { - Name = "UserService", + Message = "Call the testFn", + AttachedFunctions = new List + { + new FunctionReference { + Function = "TestFn", + Service = "default" + } + }, + // Optional: Subscribe an Inferable function to receive notifications when the run status changes + //OnStatusChange = new CreateOnStatusChangeInput + //{ + // Function = OnStatusChangeFunction + //} }); -userService.RegisterFunction(....) - -await userService.Start(); +// Wait for the run to complete and log. +var result = await run.Poll(null); ``` -To stop the service, use: - -```csharp -await userService.StopAsync(); -``` +> 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 diff --git a/sdk-dotnet/src/API/APIClient.cs b/sdk-dotnet/src/API/APIClient.cs index 03a68d3b..9b73522a 100644 --- a/sdk-dotnet/src/API/APIClient.cs +++ b/sdk-dotnet/src/API/APIClient.cs @@ -35,6 +35,12 @@ public ApiClient(ApiClientOptions options) _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", options.ApiSecret); } + async private Task RethrowWithContext(HttpRequestException e, HttpResponseMessage response) + { + throw new Exception($"Failed to get run. Response: {await response.Content.ReadAsStringAsync()}", e); + } + + async public Task CreateMachine(CreateMachineInput input) { @@ -45,7 +51,11 @@ async public Task CreateMachine(CreateMachineInput input) new StringContent(jsonData, Encoding.UTF8, "application/json") ); - response.EnsureSuccessStatusCode(); + try { + response.EnsureSuccessStatusCode(); + } catch (HttpRequestException e) { + await RethrowWithContext(e, response); + } string responseBody = await response.Content.ReadAsStringAsync(); return JsonSerializer.Deserialize(responseBody); @@ -60,7 +70,55 @@ async public Task CreateCallResult(string clusterId, string callId, CreateResult new StringContent(jsonData, Encoding.UTF8, "application/json") ); - response.EnsureSuccessStatusCode(); + try { + response.EnsureSuccessStatusCode(); + } catch (HttpRequestException e) { + await RethrowWithContext(e, response); + } + } + + async public Task CreateRun(string clusterId, CreateRunInput input) + { + string jsonData = JsonSerializer.Serialize(input); + + HttpResponseMessage response = await _client.PostAsync( + $"/clusters/{clusterId}/runs", + new StringContent(jsonData, Encoding.UTF8, "application/json") + ); + + try { + response.EnsureSuccessStatusCode(); + } catch (HttpRequestException e) { + await RethrowWithContext(e, response); + } + + string responseBody = await response.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize(responseBody); + + return result; + } + + async public Task GetRun(string clusterId, string runId) + { + HttpResponseMessage response = await _client.GetAsync( + $"/clusters/{clusterId}/runs/{runId}" + ); + + try { + try { + response.EnsureSuccessStatusCode(); + } catch (HttpRequestException e) { + await RethrowWithContext(e, response); + } + + } catch (HttpRequestException e) { + throw new Exception($"Failed to get run. Status Code: {response.StatusCode}, Response: {await response.Content.ReadAsStringAsync()}", e); + } + + string responseBody = await response.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize(responseBody); + + return result; } async public Task<(List, int?)> ListCalls(string clusterId, string service) @@ -69,7 +127,11 @@ async public Task CreateCallResult(string clusterId, string callId, CreateResult $"/clusters/{clusterId}/calls?service={service}&acknowledge=true" ); - response.EnsureSuccessStatusCode(); + try { + response.EnsureSuccessStatusCode(); + } catch (HttpRequestException e) { + await RethrowWithContext(e, response); + } string responseBody = await response.Content.ReadAsStringAsync(); var result = JsonSerializer.Deserialize>(responseBody) ?? new List(); @@ -97,7 +159,11 @@ async public Task CreateCall(string clusterId, CreateCallInput new StringContent(jsonData, Encoding.UTF8, "application/json") ); - response.EnsureSuccessStatusCode(); + try { + response.EnsureSuccessStatusCode(); + } catch (HttpRequestException e) { + await RethrowWithContext(e, response); + } string responseBody = await response.Content.ReadAsStringAsync(); return JsonSerializer.Deserialize(responseBody); diff --git a/sdk-dotnet/src/API/Models.cs b/sdk-dotnet/src/API/Models.cs index 9256ea8c..bd87dced 100644 --- a/sdk-dotnet/src/API/Models.cs +++ b/sdk-dotnet/src/API/Models.cs @@ -2,6 +2,7 @@ namespace Inferable.API { + public struct CreateMachineInput { [JsonPropertyName("service")] @@ -112,4 +113,74 @@ public struct FunctionConfig public int? TimeoutSeconds { get; set; } } + public struct CreateRunInput + { + [JsonPropertyName("message")] + public string? Message { get; set; } + + [ + JsonPropertyName("attachedFunctions"), + JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull), + ] + public List? AttachedFunctions { get; set; } + + [ + JsonPropertyName("metadata"), + JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull) + ] + public Dictionary? Metadata { get; set; } + + [ + JsonPropertyName("onStatusChange"), + JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull) + ] + public CreateOnStatusChangeInput? OnStatusChange { get; set; } + } + + public struct CreateOnStatusChangeInput + { + [ + JsonPropertyName("function"), + JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull), + ] + public FunctionReference? Function { get; set; } + } + + public struct FunctionReference + { + [JsonPropertyName("service")] + public required string Service { get; set; } + [JsonPropertyName("function")] + public required string Function { get; set; } + } + + public struct CreateRunResult + { + [JsonPropertyName("id")] + public string ID { get; set; } + } + + public struct GetRunResult + { + [JsonPropertyName("id")] + public string ID { get; set; } + + [JsonPropertyName("status")] + public string Status { get; set; } + + [JsonPropertyName("failureReason")] + public string FailureReason { get; set; } + + [JsonPropertyName("summary")] + public string Summary { get; set; } + + [JsonPropertyName("result")] + public object? Result { get; set; } + + [JsonPropertyName("attachedFunctions")] + public List AttachedFunctions { get; set; } + + [JsonPropertyName("metadata")] + public Dictionary Metadata { get; set; } + } } diff --git a/sdk-dotnet/src/Inferable.cs b/sdk-dotnet/src/Inferable.cs index 570bb504..997a1d80 100644 --- a/sdk-dotnet/src/Inferable.cs +++ b/sdk-dotnet/src/Inferable.cs @@ -1,3 +1,4 @@ +using System.Text.Json.Serialization; using Inferable.API; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -9,17 +10,46 @@ public class Links public static string DOCS_AUTH = "https://docs.inferable.ai/pages/auth"; } + /// + /// Object type that will be returned to a Run's OnStatusChange Function + /// + public struct OnStatusChangeInput + { + [JsonPropertyName("runId")] + public string RunId { get; set; } + + [JsonPropertyName("status")] + public string Status { get; set; } + + [JsonPropertyName("summary")] + public string? Summary { get; set; } + + [JsonPropertyName("result")] + public T? Result { get; set; } + + [JsonPropertyName("metadata")] + public Dictionary Metadata { get; set; } + } + public class InferableOptions { public string? BaseUrl { get; set; } public string? ApiSecret { get; set; } - /// - /// PingInterval in seconds - /// - public int? PingInterval { get; set; } public string? MachineId { get; set; } + public string? ClusterId { get; set; } + } + + public struct PollRunOptions + { + public required TimeSpan MaxWaitTime { get; set; } + public required TimeSpan Interval { get; set; } } + public class RunReference + { + public required string ID { get; set; } + public required Func> Poll { get; set; } + } public class InferableClient { @@ -27,6 +57,7 @@ public class InferableClient private readonly ApiClient _client; private readonly ILogger _logger; + private readonly string? _clusterId; // Dictionary of service name to list of functions private Dictionary> _functionRegistry = new Dictionary>(); @@ -46,6 +77,7 @@ public InferableClient(InferableOptions? options = null, ILogger CreateRun(CreateRunInput input) + { + if (this._clusterId == null) { + throw new ArgumentException("Cluster ID must be provided to manage runs"); + } + + var result = await this._client.CreateRun(this._clusterId, input); + + return new RunReference { + ID = result.ID, + Poll = async (PollRunOptions? options) => { + var MaxWaitTime = options?.MaxWaitTime ?? TimeSpan.FromSeconds(60); + var Interval = options?.Interval ?? TimeSpan.FromMilliseconds(500); + + var start = DateTime.Now; + var end = start + MaxWaitTime; + while (DateTime.Now < end) { + var pollResult = await this._client.GetRun(this._clusterId, result.ID); + + var transientStates = new List { "pending", "running" }; + if (transientStates.Contains(pollResult.Status)) { + await Task.Delay(Interval); + continue; + } + + return pollResult; + } + return null; + } + }; + } + public IEnumerable ActiveServices { get @@ -148,8 +212,13 @@ internal RegisteredService(string name, InferableClient inferable) { this._inferable = inferable; } - public void RegisterFunction(FunctionRegistration function) where T : struct { + public FunctionReference RegisterFunction(FunctionRegistration function) where T : struct { this._inferable.RegisterFunction(this._name, function); + + return new FunctionReference { + Service = this._name, + Function = function.Name + }; } async public Task Start() { diff --git a/sdk-dotnet/tests/Inferable.Tests/InferableTest.cs b/sdk-dotnet/tests/Inferable.Tests/InferableTest.cs index 9b7538d4..9877ba6b 100644 --- a/sdk-dotnet/tests/Inferable.Tests/InferableTest.cs +++ b/sdk-dotnet/tests/Inferable.Tests/InferableTest.cs @@ -16,12 +16,12 @@ static InferableTests() { Env.Load(EnvFilePath); ApiClient = new ApiClient(new ApiClientOptions{ - ApiSecret = System.Environment.GetEnvironmentVariable("INFERABLE_CONSUME_SECRET")!, - BaseUrl = System.Environment.GetEnvironmentVariable("INFERABLE_API_ENDPOINT")!, + ApiSecret = System.Environment.GetEnvironmentVariable("INFERABLE_TEST_API_SECRET")!, + BaseUrl = System.Environment.GetEnvironmentVariable("INFERABLE_TEST_API_ENDPOINT")!, MachineId = "test" }); - TestClusterId = System.Environment.GetEnvironmentVariable("INFERABLE_CLUSTER_ID")!; + TestClusterId = System.Environment.GetEnvironmentVariable("INFERABLE_TEST_CLUSTER_ID")!; } static InferableClient CreateInferableClient() @@ -33,7 +33,9 @@ static InferableClient CreateInferableClient() }).CreateLogger(); return new InferableClient(new InferableOptions { - ApiSecret = System.Environment.GetEnvironmentVariable("INFERABLE_MACHINE_SECRET")!, + ApiSecret = System.Environment.GetEnvironmentVariable("INFERABLE_TEST_API_SECRET")!, + BaseUrl = System.Environment.GetEnvironmentVariable("INFERABLE_TEST_API_ENDPOINT")!, + ClusterId = System.Environment.GetEnvironmentVariable("INFERABLE_TEST_CLUSTER_ID")!, }, logger); } @@ -214,6 +216,69 @@ async public void Inferable_Can_Handle_Functions_Failure() await inferable.Default.Stop(); } } + + /// + /// End to end test of the Inferable SDK + /// - Can a Run be triggered + /// - Can a Function be called + /// - Can a StatusChange function be called + /// + [Fact] + async public void Inferable_Run_E2E() + { + var inferable = CreateInferableClient(); + + bool didCallSuccessFunction = false; + bool didCallOnStatusChange = false; + + var Testfn = inferable.Default.RegisterFunction( new FunctionRegistration + { + Name = "testFn", + Func = new Func((input) => + { + didCallSuccessFunction = true; + return "This is a test response"; + }) + }); + + var OnStatusChangeFunction = inferable.Default.RegisterFunction(new FunctionRegistration> + { + Name = "onStatusChangeFn", + Func = new Func, object?>((input) => + { + didCallOnStatusChange = true; + return null; + }), + }); + + try + { + await inferable.Default.Start(); + + var run = await inferable.CreateRun(new CreateRunInput + { + Message = "Call the testFn", + AttachedFunctions = new List + { + Testfn + }, + OnStatusChange = new CreateOnStatusChangeInput + { + Function = OnStatusChangeFunction + } + }); + + var result = await run.Poll(null); + + Assert.NotNull(result); + Assert.True(didCallSuccessFunction); + Assert.True(didCallOnStatusChange); + } + finally + { + await inferable.Default.Stop(); + } + } } //TODO: Test transient /call failures From 89157ba0c6acfe68ae7f37c3e3a8b61e7e498d89 Mon Sep 17 00:00:00 2001 From: John Smith Date: Thu, 31 Oct 2024 15:45:09 +1030 Subject: [PATCH 2/4] chore: Bump project version --- sdk-dotnet/src/Inferable.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk-dotnet/src/Inferable.csproj b/sdk-dotnet/src/Inferable.csproj index d0996105..dded0384 100644 --- a/sdk-dotnet/src/Inferable.csproj +++ b/sdk-dotnet/src/Inferable.csproj @@ -6,7 +6,7 @@ enable Inferable - 0.0.10 + 0.0.11 Inferable Inferable Client library for interacting with the Inferable API From e08fa9c0a9fd231cb6934020fa6af9f8723476a0 Mon Sep 17 00:00:00 2001 From: John Smith Date: Thu, 31 Oct 2024 16:28:55 +1030 Subject: [PATCH 3/4] chore: Standardise Node and Dotnet readmes --- .github/workflows/build.yml | 7 +- sdk-dotnet/README.md | 30 +++++---- .../tests/Inferable.Tests/InferableTest.cs | 35 +++++----- sdk-node/README.md | 52 +++++++-------- sdk-node/src/Inferable.test.ts | 66 +++++++++++++++++-- sdk-node/src/Inferable.ts | 32 +++------ sdk-node/src/create-client.ts | 3 + sdk-node/src/index.ts | 2 +- sdk-node/src/tests/utils.ts | 21 +++--- sdk-node/src/types.ts | 12 ++++ 10 files changed, 161 insertions(+), 99 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2b3e3c11..f5f9b4f6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -85,10 +85,9 @@ jobs: - name: Run tests run: npm run test 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 }} build-dotnet: needs: check_changes diff --git a/sdk-dotnet/README.md b/sdk-dotnet/README.md index da56c598..4a3f31a6 100644 --- a/sdk-dotnet/README.md +++ b/sdk-dotnet/README.md @@ -43,7 +43,7 @@ If you don't provide an API key or base URL, it will attempt to read them from t ### Registering a Function -To register a function with the Inferable API, you can use the following: +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). ```csharp public class MyInput @@ -53,11 +53,11 @@ public class MyInput client.Default.RegisterFunction(new FunctionRegistration { - Function = new Func>((input) => { - // Your code here - }), Name = "SayHello", Description = "A simple greeting function", + Func = new Func>((input) => { + // Your code here + }), }); await client.Default.Start(); @@ -65,7 +65,7 @@ await client.Default.Start(); ### 3. Trigger a run -The following code will create an [Inferable run](https://docs.inferable.ai/pages/runs) with the message "Call the testFn" and the `TestFn` function attached. +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. > You can inspect the progress of the run: > @@ -75,11 +75,11 @@ The following code will create an [Inferable run](https://docs.inferable.ai/page ```csharp var run = await inferable.CreateRun(new CreateRunInput { - Message = "Call the testFn", + Message = "Say hello to John", AttachedFunctions = new List { new FunctionReference { - Function = "TestFn", + Function = "SayHello", Service = "default" } }, @@ -90,16 +90,24 @@ var run = await inferable.CreateRun(new CreateRunInput //} }); -// Wait for the run to complete and log. +Console.WriteLine($"Run started: {run.Id}"); + +// Wait for the run to complete and log var result = await run.Poll(null); + +Console.WriteLine($"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 .NET 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/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 .NET Client are welcome. Please ensure that your code adheres to the existing style and includes appropriate tests. diff --git a/sdk-dotnet/tests/Inferable.Tests/InferableTest.cs b/sdk-dotnet/tests/Inferable.Tests/InferableTest.cs index 9877ba6b..9898d305 100644 --- a/sdk-dotnet/tests/Inferable.Tests/InferableTest.cs +++ b/sdk-dotnet/tests/Inferable.Tests/InferableTest.cs @@ -222,26 +222,27 @@ async public void Inferable_Can_Handle_Functions_Failure() /// - Can a Run be triggered /// - Can a Function be called /// - Can a StatusChange function be called + /// This should match the example in the readme /// [Fact] async public void Inferable_Run_E2E() { - var inferable = CreateInferableClient(); + var client = CreateInferableClient(); - bool didCallSuccessFunction = false; + bool didCallSayHello = false; bool didCallOnStatusChange = false; - var Testfn = inferable.Default.RegisterFunction( new FunctionRegistration + var SayHelloFunction = client.Default.RegisterFunction(new FunctionRegistration { - Name = "testFn", - Func = new Func((input) => - { - didCallSuccessFunction = true; - return "This is a test response"; - }) + Name = "SayHello", + Description = "A simple greeting function", + Func = new Func((input) => { + didCallSayHello = true; + return null; + }), }); - var OnStatusChangeFunction = inferable.Default.RegisterFunction(new FunctionRegistration> + var OnStatusChangeFunction = client.Default.RegisterFunction(new FunctionRegistration> { Name = "onStatusChangeFn", Func = new Func, object?>((input) => @@ -253,14 +254,14 @@ async public void Inferable_Run_E2E() try { - await inferable.Default.Start(); + await client.Default.Start(); - var run = await inferable.CreateRun(new CreateRunInput + var run = await client.CreateRun(new CreateRunInput { - Message = "Call the testFn", + Message = "Say hello to John", AttachedFunctions = new List { - Testfn + SayHelloFunction }, OnStatusChange = new CreateOnStatusChangeInput { @@ -270,13 +271,15 @@ async public void Inferable_Run_E2E() var result = await run.Poll(null); + await Task.Delay(500); + Assert.NotNull(result); - Assert.True(didCallSuccessFunction); + Assert.True(didCallSayHello); Assert.True(didCallOnStatusChange); } finally { - await inferable.Default.Stop(); + await client.Default.Stop(); } } } diff --git a/sdk-node/README.md b/sdk-node/README.md index 5e860d1e..41dd9e88 100644 --- a/sdk-node/README.md +++ b/sdk-node/README.md @@ -35,8 +35,6 @@ pnpm add inferable ### 1. Initializing Inferable -Create a file named i.ts which will be used to initialize Inferable. This file will export the Inferable instance. - ```typescript // d.ts @@ -44,29 +42,27 @@ import { Inferable } from "inferable"; // Initialize the Inferable client with your API secret. // Get yours at https://console.inferable.ai. -export const d = new Inferable({ +const client = new Inferable({ apiSecret: "YOUR_API_SECRET", }); ``` -### 2. Hello World Function - -In a separate file, register a "sayHello" [function](https://docs.inferable.ai/pages/functions). This file will import the Inferable instance from `i.ts` and register the [function](https://docs.inferable.ai/pages/functions) with the [control-plane](https://docs.inferable.ai/pages/control-plane). +If you don't provide an API key or base URL, it will attempt to read them from the following environment variables: -```typescript -// service.ts +- `INFERABLE_API_SECRET` +- `INFERABLE_API_ENDPOINT` -import { i } from "./i"; +### 2. Hello World Function -// Define a simple function that returns "Hello, World!" -const sayHello = async ({ to }: { to: string }) => { - return `Hello, ${to}!`; -}; +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). -// Register the service (using the 'default' service) -const sayHello = i.default.register({ +```typescript +// Register a simple function (using the 'default' service) +const sayHello = client.default.register({ name: "sayHello", - func: sayHello, + func: async ({ to }: { to: string }) => { + return `Hello, ${to}!`; + }, schema: { input: z.object({ to: z.string(), @@ -75,18 +71,10 @@ const sayHello = i.default.register({ }); // Start the 'default' service -i.default.start(); -``` - -### 3. Running the Service - -To run the service, simply run the file with the [function](https://docs.inferable.ai/pages/functions) definition. This will start the `default` [service](https://docs.inferable.ai/pages/services) and make it available to the Inferable agent. - -```bash -tsx service.ts +client.default.start(); ``` -### 4. Trigger a run +### 3. Trigger 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. @@ -96,7 +84,7 @@ 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 -const run = await i.run({ +const run = await client.run({ message: "Say hello to John", // Optional: Explicitly attach the `sayHello` function (All functions attached by default) attachedFunctions: [{ @@ -111,7 +99,7 @@ const run = await i.run({ //onStatusChange: { function: { function: "handler", service: "default" } }, }); -console.log("Started Run", { +console.log("Run Started", { result: run.id, }); @@ -126,3 +114,11 @@ console.log("Run result", { ## Documentation - [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/issues) or [join the Discord](https://discord.gg/WHcTNeDP) + +## Contributing + +Contributions to the Inferable NodeJs Client are welcome. Please ensure that your code adheres to the existing style and includes appropriate tests. diff --git a/sdk-node/src/Inferable.test.ts b/sdk-node/src/Inferable.test.ts index 2dcdd250..ff1d5687 100644 --- a/sdk-node/src/Inferable.test.ts +++ b/sdk-node/src/Inferable.test.ts @@ -3,13 +3,13 @@ import { z } from "zod"; import { Inferable } from "./Inferable"; import { TEST_CLUSTER_ID, - TEST_CONSUME_SECRET, - TEST_MACHINE_SECRET, + TEST_API_SECRET, client, inferableInstance, } from "./tests/utils"; import { setupServer } from "msw/node"; import { http, HttpResponse, passthrough } from "msw"; +import { statusChangeSchema } from "./types"; const testService = () => { const inferable = inferableInstance(); @@ -57,12 +57,12 @@ describe("Inferable", () => { it("should initialize without optional args", () => { expect( - () => new Inferable({ apiSecret: TEST_MACHINE_SECRET }), + () => new Inferable({ apiSecret: TEST_API_SECRET }), ).not.toThrow(); }); it("should initialize with API secret in environment", () => { - process.env.INFERABLE_API_SECRET = TEST_MACHINE_SECRET; + process.env.INFERABLE_API_SECRET = TEST_API_SECRET; expect(() => new Inferable()).not.toThrow(); }); @@ -239,3 +239,61 @@ describe("Functions", () => { server.close(); }); }); + +// This should match the example in the readme +describe("Inferable SDK End to End Test", () => { + it("should trigger a run, call a function, and call a status change function", async () => { + const client = inferableInstance(); + + let didCallSayHello = false; + let didCallOnStatusChange = false; + + // Register a simple function (using the 'default' service) + const sayHello = client.default.register({ + name: "sayHello", + func: async ({ to }: { to: string }) => { + didCallSayHello = true; + return `Hello, ${to}!`; + }, + schema: { + input: z.object({ + to: z.string(), + }), + }, + }); + + const onStatusChange = client.default.register({ + name: "onStatusChangeFn", + schema: statusChangeSchema, + func: (_input) => { + didCallOnStatusChange = true; + }, + }); + + try { + await client.default.start(); + + const run = await client.run({ + message: "Say hello to John", + // Optional: Explicitly attach the `sayHello` function (All functions attached by default) + attachedFunctions: [sayHello], + // Optional: Define a schema for the result to conform to + resultSchema: z.object({ + didSayHello: z.boolean() + }), + // Optional: Subscribe an Inferable function to receive notifications when the run status changes + onStatusChange: { function: onStatusChange }, + }); + + const result = await run.poll(); + + await new Promise((resolve) => setTimeout(resolve, 500)); + + expect(result).not.toBeNull(); + expect(didCallSayHello).toBe(true); + expect(didCallOnStatusChange).toBe(true); + } finally { + await client.default.stop(); + } + }); +}); diff --git a/sdk-node/src/Inferable.ts b/sdk-node/src/Inferable.ts index d3b28eb1..fa878e1b 100644 --- a/sdk-node/src/Inferable.ts +++ b/sdk-node/src/Inferable.ts @@ -30,7 +30,6 @@ debug.formatters.J = (json) => { export const log = debug("inferable:client"); - type RunInput = Omit["createRun"]>[0] >["body"], "resultSchema"> & { @@ -38,10 +37,6 @@ type RunInput = Omit | JsonSchemaInput }; -type TemplateRunInput = Omit & { - input: Record; -}; - /** * The Inferable client. This is the main entry point for using Inferable. * @@ -217,23 +212,10 @@ export class Inferable { }); } - /** - * Creates a template reference. This can be used to trigger runs of a template that was previously registered via the UI. - * @param id The ID of the template to reference. - * @returns A referenced template instance. - */ - public async template(template: { id: string }) { - return { - id: template.id, - run: (input: TemplateRunInput) => - this.run({ ...input, template: { id: template.id, input: input.input } }), - }; - } - /** * Creates a run (or retrieves an existing run if an ID is provided) and returns a reference to it. * @param input The run definition. - * @returns A run handle. + * @returns A run reference. * @example * ```ts * const d = new Inferable({apiSecret: "API_SECRET"}); @@ -302,14 +284,18 @@ export class Inferable { * @param maxWaitTime The maximum amount of time to wait for the run to reach a terminal state. Defaults to 60 seconds. * @param interval The amount of time to wait between polling attempts. Defaults to 500ms. */ - poll: async (options: { maxWaitTime?: number, interval?: number }) => { + poll: async (options?: { maxWaitTime?: number, interval?: number }) => { + if (!this.clusterId) { + throw new InferableError("Cluster ID must be provided to manage runs"); + } + const start = Date.now(); - const end = start + (options.maxWaitTime || 60_000); + const end = start + (options?.maxWaitTime || 60_000); while (Date.now() < end) { const pollResult = await this.client.getRun({ params: { - clusterId: process.env.INFERABLE_CLUSTER_ID!, + clusterId: this.clusterId, runId: runResult.body.id, }, }); @@ -322,7 +308,7 @@ export class Inferable { } if (["pending", "running"].includes(pollResult.body.status ?? "")) { await new Promise((resolve) => { - setTimeout(resolve, options.interval || 500); + setTimeout(resolve, options?.interval || 500); }); continue; } diff --git a/sdk-node/src/create-client.ts b/sdk-node/src/create-client.ts index 5734a5cc..cc49c201 100644 --- a/sdk-node/src/create-client.ts +++ b/sdk-node/src/create-client.ts @@ -2,6 +2,9 @@ import { initClient, tsRestFetchApi } from "@ts-rest/core"; import { contract } from "./contract"; const { version: SDK_VERSION } = require("../package.json"); +/** + * Provides raw API access to the Inferable API. + */ export const createApiClient = ({ baseUrl, machineId, diff --git a/sdk-node/src/index.ts b/sdk-node/src/index.ts index b8f0c7e1..550bc120 100644 --- a/sdk-node/src/index.ts +++ b/sdk-node/src/index.ts @@ -17,11 +17,11 @@ */ export { Inferable } from "./Inferable"; + export const masked = () => { throw new Error("masked is not implemented"); }; -export * as InferablePromptfooProvider from "./eval/promptfoo"; export { statusChangeSchema } from "./types"; export { diff --git a/sdk-node/src/tests/utils.ts b/sdk-node/src/tests/utils.ts index 09606983..9828492c 100644 --- a/sdk-node/src/tests/utils.ts +++ b/sdk-node/src/tests/utils.ts @@ -3,19 +3,16 @@ import { initClient } from "@ts-rest/core"; import { contract } from "../contract"; if ( - !process.env.INFERABLE_MACHINE_SECRET || - !process.env.INFERABLE_CONSUME_SECRET || - !process.env.INFERABLE_API_ENDPOINT || - !process.env.INFERABLE_CLUSTER_ID + !process.env.INFERABLE_TEST_API_SECRET || + !process.env.INFERABLE_TEST_API_ENDPOINT || + !process.env.INFERABLE_TEST_CLUSTER_ID ) { throw new Error("Test environment variables not set"); } -export const TEST_ENDPOINT = process.env.INFERABLE_API_ENDPOINT; -export const TEST_CLUSTER_ID = process.env.INFERABLE_CLUSTER_ID; - -export const TEST_MACHINE_SECRET = process.env.INFERABLE_MACHINE_SECRET; -export const TEST_CONSUME_SECRET = process.env.INFERABLE_CONSUME_SECRET; +export const TEST_ENDPOINT = process.env.INFERABLE_TEST_API_ENDPOINT; +export const TEST_CLUSTER_ID = process.env.INFERABLE_TEST_CLUSTER_ID; +export const TEST_API_SECRET = process.env.INFERABLE_TEST_API_SECRET; console.log("Testing with", { TEST_ENDPOINT, @@ -25,13 +22,13 @@ console.log("Testing with", { export const client = initClient(contract, { baseUrl: TEST_ENDPOINT, baseHeaders: { - authorization: `${TEST_CONSUME_SECRET}`, + authorization: `${TEST_API_SECRET}`, }, }); export const inferableInstance = () => new Inferable({ - apiSecret: TEST_MACHINE_SECRET, + apiSecret: TEST_API_SECRET, endpoint: TEST_ENDPOINT, - jobPollWaitTime: 5000, + clusterId: TEST_CLUSTER_ID, }); diff --git a/sdk-node/src/types.ts b/sdk-node/src/types.ts index d9ab6bab..085acab2 100644 --- a/sdk-node/src/types.ts +++ b/sdk-node/src/types.ts @@ -11,6 +11,18 @@ export type FunctionInput = : // eslint-disable-next-line @typescript-eslint/no-explicit-any any; +/** + * Schema type that will be returned to a Run's OnStatusChange Function + * + * @example + * ```ts + * inferable.default.register({ + * name: "onStatusChangeFn", + * schema: statusChangeSchema, + * func: (_input) => {}, + * }); + * ``` + */ export const statusChangeSchema = { input: z.object({ runId: z.string(), From fcacf5c56179179a7bfde59765376ab928e0cc5f Mon Sep 17 00:00:00 2001 From: John Smith Date: Thu, 31 Oct 2024 16:31:23 +1030 Subject: [PATCH 4/4] chore: Add test result --- sdk-dotnet/tests/Inferable.Tests/InferableTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk-dotnet/tests/Inferable.Tests/InferableTest.cs b/sdk-dotnet/tests/Inferable.Tests/InferableTest.cs index 9898d305..f5be14af 100644 --- a/sdk-dotnet/tests/Inferable.Tests/InferableTest.cs +++ b/sdk-dotnet/tests/Inferable.Tests/InferableTest.cs @@ -238,7 +238,7 @@ async public void Inferable_Run_E2E() Description = "A simple greeting function", Func = new Func((input) => { didCallSayHello = true; - return null; + return $"Hello {input.testString}"; }), });