From 72f905946b6456e6c84649ef404d62f79dcbb55f Mon Sep 17 00:00:00 2001 From: Nadeesha Cabral Date: Mon, 9 Dec 2024 13:54:20 +1100 Subject: [PATCH 1/5] fix: Simplify the quickstart --- bootstrap-node/src/functions.ts | 58 ---------------- bootstrap-node/src/index.ts | 55 ++++++--------- bootstrap-node/src/run.ts | 117 +++++--------------------------- 3 files changed, 36 insertions(+), 194 deletions(-) delete mode 100644 bootstrap-node/src/functions.ts diff --git a/bootstrap-node/src/functions.ts b/bootstrap-node/src/functions.ts deleted file mode 100644 index 2fc4b59e..00000000 --- a/bootstrap-node/src/functions.ts +++ /dev/null @@ -1,58 +0,0 @@ -export async function getUrlContent({ url }: { url: string }) { - const response = await fetch(url); - - if (!response.ok) { - return { - supervisor: - "If the error is retryable, try again. If not, tell the user why this failed.", - message: `Failed to fetch ${url}: ${response.statusText}`, - response, - }; - } - - const html = await response.text(); - - return { - body: html.replace(/<(?!a\s|\/a\s|a>|\/a>)[^>]*>/g, ""), - }; -} - -export function scoreHNPost({ - commentCount, - upvotes, -}: { - commentCount: number; - upvotes: number; -}) { - return upvotes + commentCount * 2; -} - -export async function generatePage({ markdown }: { markdown: string }) { - const html = ` - - - Hacker News Page Generated by Inferable - - - -
${markdown}
- - - - `; - - const tmpPath = require("path").join( - require("os").tmpdir(), - "inferable-hacker-news.html", - ); - - await require("fs").promises.writeFile(tmpPath, html); - - return { - message: `Tell the user to open the file at tmpPath in their browser.`, - tmpPath, - }; -} diff --git a/bootstrap-node/src/index.ts b/bootstrap-node/src/index.ts index b008fe52..e6d94f67 100644 --- a/bootstrap-node/src/index.ts +++ b/bootstrap-node/src/index.ts @@ -1,48 +1,35 @@ import { Inferable } from "inferable"; import { z } from "zod"; +import { execFile } from "child_process"; +import { promisify } from "util"; -// Some mock functions to register -import * as functions from "./functions"; +const execFilePromise = promisify(execFile); -// Instantiate the Inferable client. const client = new Inferable({ - // To get a new key, run: - // npx @inferable/cli auth keys create 'My New Machine Key' --type='cluster_machine' + // Get your key from https://app.inferable.ai/clusters apiSecret: process.env.INFERABLE_API_SECRET, }); -// Register some demo functions client.default.register({ - name: "getUrlContent", - func: functions.getUrlContent, - description: "Gets the content of a URL", - schema: { - input: z.object({ - url: z.string().describe("The URL to get the content of"), - }), - }, -}); - -client.default.register({ - name: "generatePage", - func: functions.generatePage, - description: "Generates a page from markdown", - schema: { - input: z.object({ - markdown: z.string().describe("The markdown to generate a page from"), - }), + name: "exec", + func: async ({ command, arg }: { command: string; arg?: string }) => { + const args = arg ? [arg] : []; + const { stdout, stderr } = await execFilePromise(command, args); + return { + stdout: stdout.trim(), + stderr: stderr.trim(), + }; }, -}); - -client.default.register({ - name: "scoreHNPost", - func: functions.scoreHNPost, - description: - "Calculates a score for a Hacker News post given its comment count and upvotes", + description: "Executes a system command", schema: { input: z.object({ - commentCount: z.number().describe("The number of comments"), - upvotes: z.number().describe("The number of upvotes"), + command: z + .enum(["pwd", "ls", "cat", "echo"]) // This prevents arbitrary commands + .describe("The command to execute"), + arg: z + .string() + .describe("The argument to pass to the command") + .optional(), }), }, }); @@ -50,5 +37,3 @@ client.default.register({ client.default.start().then(() => { console.log("Inferable demo service started"); }); - -// To trigger a run: tsx -r dotenv/config src/run.ts diff --git a/bootstrap-node/src/run.ts b/bootstrap-node/src/run.ts index aed1cf04..e419ff61 100644 --- a/bootstrap-node/src/run.ts +++ b/bootstrap-node/src/run.ts @@ -1,114 +1,29 @@ import { Inferable } from "inferable"; import { z } from "zod"; -import { exec } from "child_process"; const client = new Inferable({ apiSecret: process.env.INFERABLE_API_SECRET, }); -const postsSchema = z.object({ - id: z.string().describe("The id of the post"), - title: z.string().describe("The title of the post"), - points: z.string().describe("The key points from the comments"), - commentsUrl: z - .string() - .describe( - "The URL of the comments. This is typically https://news.ycombinator.com/item?id=", - ), +const reportSchema = z.object({ + name: z.string(), + capabilities: z + .array(z.string()) + .describe("The capabilities of the program. What it can do."), }); -// Trigger a Run programmatically -// https://docs.inferable.ai/pages/runs - -const extract = async () => - client - .run({ - initialPrompt: ` - Hacker News has a homepage at https://news.ycombinator.com/ - Each post has a id, title, a link, and a score, and is voted on by users. - Score the top 10 posts and pick the top 3 according to the internal scoring function. - `, - resultSchema: z.object({ - posts: postsSchema.array(), - }), - callSummarization: false, - }) - .then( - (r) => - r.poll() as Promise<{ - result: { - posts?: z.infer[]; - }; - }>, - ); - -const summarizePost = async ({ data }: { data: object }) => - client - .run({ - initialPrompt: ` - - ${JSON.stringify(data).substring(0, 20_000)} - - - You are given a post from Hacker News, and a url for the post's comments. - Summarize the comments. You should visit the comments URL to get the comments. - Produce a list of the key points from the comments. - `, - resultSchema: z.object({ - id: z.string().describe("The id of the post"), - title: z.string().describe("The title of the post"), - keyPoints: z - .array(z.string()) - .describe("The key points from the comments"), - }), - }) - .then((r) => r.poll()); - -const generatePage = async ({ data }: { data: object }) => - client - .run({ - initialPrompt: ` - - ${JSON.stringify(data)} - - - You are given a list of posts from Hacker News, and a summary of the comments for each post. - - Generate a web page with the following structure: - - A header with the title of the page - - A list of posts, with the title, a link to the post, and the key points from the comments in a ul - - A footer with a link to the original Hacker News page - `, - resultSchema: z.object({ - pagePath: z.string().describe("The path of the generated web page"), - }), - }) - .then((r) => r.poll()); - -const url = process.env.INFERABLE_CLUSTER_ID - ? `https://app.inferable.ai/clusters/${process.env.INFERABLE_CLUSTER_ID}/runs` - : "https://app.inferable.ai/clusters"; - -// open the page in the browser -exec(`open ${url}`, (error) => { - if (error) { - console.error("Failed to open browser:", error); - } -}); - -extract() - .then(({ result }) => { - if (!result.posts) { - throw new Error("No posts found"); - } - - return Promise.all( - result.posts.map((post) => summarizePost({ data: post })), - ); +client + .run({ + initialPrompt: ` + Iteratively inspect the files at the current directory, and produce a report. + You may selectively inspect the contents of files. + `.trim(), + resultSchema: reportSchema, }) + .then((r) => r.poll()) .then((result) => { - return generatePage({ data: result }); + console.log(result); }) - .then((result) => { - console.log("Generated page", result); + .catch((error) => { + console.error(error); }); From 996462b6295bb9eae359b0760ead105018324a73 Mon Sep 17 00:00:00 2001 From: Nadeesha Cabral Date: Mon, 9 Dec 2024 14:42:54 +1100 Subject: [PATCH 2/5] update --- bootstrap-node/src/index.ts | 14 ++++++-------- bootstrap-node/src/run.ts | 4 ++-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/bootstrap-node/src/index.ts b/bootstrap-node/src/index.ts index e6d94f67..93c8646d 100644 --- a/bootstrap-node/src/index.ts +++ b/bootstrap-node/src/index.ts @@ -2,6 +2,7 @@ import { Inferable } from "inferable"; import { z } from "zod"; import { execFile } from "child_process"; import { promisify } from "util"; +import assert from "assert"; const execFilePromise = promisify(execFile); @@ -12,9 +13,9 @@ const client = new Inferable({ client.default.register({ name: "exec", - func: async ({ command, arg }: { command: string; arg?: string }) => { - const args = arg ? [arg] : []; - const { stdout, stderr } = await execFilePromise(command, args); + func: async ({ command, arg }: { command: string; arg: string }) => { + assert(arg.startsWith("./"), "can only access paths starting with ./"); + const { stdout, stderr } = await execFilePromise(command, [arg]); return { stdout: stdout.trim(), stderr: stderr.trim(), @@ -24,12 +25,9 @@ client.default.register({ schema: { input: z.object({ command: z - .enum(["pwd", "ls", "cat", "echo"]) // This prevents arbitrary commands + .enum(["ls", "cat"]) // This prevents arbitrary commands .describe("The command to execute"), - arg: z - .string() - .describe("The argument to pass to the command") - .optional(), + arg: z.string().describe("The argument to pass to the command"), }), }, }); diff --git a/bootstrap-node/src/run.ts b/bootstrap-node/src/run.ts index e419ff61..46010b3d 100644 --- a/bootstrap-node/src/run.ts +++ b/bootstrap-node/src/run.ts @@ -15,8 +15,8 @@ const reportSchema = z.object({ client .run({ initialPrompt: ` - Iteratively inspect the files at the current directory, and produce a report. - You may selectively inspect the contents of files. + Iteratively inspect the source code at the current directory, and produce a report. + You may selectively inspect the contents of files. You can only access files starting with ./ `.trim(), resultSchema: reportSchema, }) From db6fb7aed6a945dfc05a19fea848f745fd0493c8 Mon Sep 17 00:00:00 2001 From: Nadeesha Cabral Date: Mon, 9 Dec 2024 14:59:09 +1100 Subject: [PATCH 3/5] update --- bootstrap-node/README.md | 61 ++++++++++++++++++++++--------------- bootstrap-node/src/index.ts | 1 + 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/bootstrap-node/README.md b/bootstrap-node/README.md index f429b9c1..d136c696 100644 --- a/bootstrap-node/README.md +++ b/bootstrap-node/README.md @@ -8,23 +8,27 @@ This is a Node.js bootstrap application that demonstrates how to integrate and u ## The Application -The application is a simple Node.js application that extracts the top posts from Hacker News and summarizes the comments for each post. It demonstrates how to: +The application demonstrates an agent that can inspect and analyze source code by iteratively executing system commands. It shows how to: - Register Typescript functions with Inferable -- Trigger a Run programmatically to orchestrate the functions -- Control the control flow of the Run using native Node.js control flow primitives +- Trigger a Run programmatically to provide a goal +- Restrict the agent's access to the filesystem using source code ```mermaid sequenceDiagram - participant extract - participant summarizePost - participant generatePage - - extract->>extract: Get top 3 HN posts - extract->>summarizePost: Posts data - summarizePost->>summarizePost: Get & analyze comments - summarizePost->>generatePage: Summaries data - generatePage->>generatePage: Generate HTML + participant Agent + participant exec + participant FileSystem + + Agent->>exec: Request file listing (ls) + exec->>FileSystem: Execute ls command + FileSystem->>exec: Return file list + exec->>Agent: File list results + Agent->>exec: Request file contents (cat) + exec->>FileSystem: Execute cat command + FileSystem->>exec: Return file contents + exec->>Agent: File contents + Agent->>Agent: Analyze code and generate report ``` ## How to Run @@ -43,20 +47,29 @@ npm run run ## How it works -1. The worker machine uses the Inferable Node.js SDK to register the functions with Inferable. These functions are: +1. The worker machine uses the Inferable Node.js SDK to register the `exec` function with Inferable. This function: -- `getUrlContent`: Get the html content of any url -- `scoreHNPost`: Score a post based on the number of comments and upvotes -- `generatePage`: Generate an HTML page with the summaries and save it to a tmp file in your OS's temp directory + - Accepts `ls` or `cat` commands with path arguments + - Only allows accessing paths that start with "./" + - Returns the stdout and stderr from the command execution -2. The `run.ts` script defines "Runs" with the Inferable Node.js SDK. These are: +2. The `run.ts` script creates a Re-Act agent that: -- `extract`: Extracts the top 3 HN posts -- `summarizePost`: Summarizes the comments for a given post -- `generatePage`: Generates an HTML page from the summaries + - Receives an initial prompt to inspect source code in the current directory + - Can iteratively call the `exec` function to list and read files + - Produces a final report containing: + - The name of the program + - A list of its capabilities -3. Given the run configuration (prompts, result schema, etc), the worker machine will orchestrate the functions to generate the page. +3. The agent will: -- `extract` will get the top 3 HN posts using the `getUrlContent` function, and score them using the `scoreHNPost` function -- `summarizePost` will summarize the comments for each post using the `getUrlContent` function -- `generatePage` will generate the HTML page using the `generatePage` function + - Use `ls` to discover files in the directory + - Use `cat` to read the contents of relevant files + - Analyze the code to understand its functionality + - Generate a structured report based on its findings + +## Security + +- The `exec` function is restricted to only allow access to files starting with "./" +- The agent is designed to be safe and only perform actions that are relevant to the task +- The constraints are enforced by source code, and cannot be bypassed by the agent diff --git a/bootstrap-node/src/index.ts b/bootstrap-node/src/index.ts index 93c8646d..7f81492c 100644 --- a/bootstrap-node/src/index.ts +++ b/bootstrap-node/src/index.ts @@ -16,6 +16,7 @@ client.default.register({ func: async ({ command, arg }: { command: string; arg: string }) => { assert(arg.startsWith("./"), "can only access paths starting with ./"); const { stdout, stderr } = await execFilePromise(command, [arg]); + return { stdout: stdout.trim(), stderr: stderr.trim(), From b1a970297be877d58f70953d798ed59da24e8c50 Mon Sep 17 00:00:00 2001 From: Nadeesha Cabral Date: Mon, 9 Dec 2024 18:44:26 +1100 Subject: [PATCH 4/5] update --- bootstrap-dotnet/ExecService.cs | 76 ++++++++ bootstrap-dotnet/HackerNewsService.cs | 73 -------- bootstrap-dotnet/Program.cs | 23 +-- bootstrap-dotnet/README.md | 122 +++++++++---- bootstrap-dotnet/Register.cs | 80 ++------ bootstrap-dotnet/Run.cs | 176 ------------------ bootstrap-dotnet/Trigger.cs | 45 +++++ bootstrap-go/README.md | 70 ++++--- bootstrap-go/cmd/run.go | 213 ---------------------- bootstrap-go/cmd/trigger.go | 68 +++++++ bootstrap-go/functions.go | 102 ----------- bootstrap-go/main.go | 80 +++++--- bootstrap-node/README.md | 13 +- bootstrap-node/package.json | 4 +- bootstrap-node/src/{run.ts => trigger.ts} | 2 +- 15 files changed, 400 insertions(+), 747 deletions(-) create mode 100644 bootstrap-dotnet/ExecService.cs delete mode 100644 bootstrap-dotnet/HackerNewsService.cs delete mode 100644 bootstrap-dotnet/Run.cs create mode 100644 bootstrap-dotnet/Trigger.cs delete mode 100644 bootstrap-go/cmd/run.go create mode 100644 bootstrap-go/cmd/trigger.go delete mode 100644 bootstrap-go/functions.go rename bootstrap-node/src/{run.ts => trigger.ts} (94%) diff --git a/bootstrap-dotnet/ExecService.cs b/bootstrap-dotnet/ExecService.cs new file mode 100644 index 00000000..523e35eb --- /dev/null +++ b/bootstrap-dotnet/ExecService.cs @@ -0,0 +1,76 @@ +using System; +using System.Diagnostics; +using System.Collections.Generic; +using System.Threading.Tasks; + +public static class ExecService +{ + private static readonly HashSet AllowedCommands = new HashSet + { + "ls", + "cat" + }; + + public static ExecResponse Exec(ExecInput input) + { + try + { + if (!AllowedCommands.Contains(input.Command)) + { + return new ExecResponse + { + Error = $"Command '{input.Command}' is not allowed. Only 'ls' and 'cat' are permitted." + }; + } + + if (!input.Arg.StartsWith("./")) + { + return new ExecResponse + { + Error = "Can only access paths starting with ./" + }; + } + + var startInfo = new ProcessStartInfo + { + FileName = input.Command, + Arguments = input.Arg, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + var stdout = process.StandardOutput.ReadToEnd().Trim(); + var stderr = process.StandardError.ReadToEnd().Trim(); + process.WaitForExit(); + + return new ExecResponse + { + Stdout = stdout, + Stderr = stderr + }; + } + catch (Exception ex) + { + return new ExecResponse + { + Error = ex.Message + }; + } + } +} + +public class ExecInput +{ + public string Command { get; set; } + public string Arg { get; set; } +} + +public class ExecResponse +{ + public string Stdout { get; set; } + public string Stderr { get; set; } + public string Error { get; set; } +} diff --git a/bootstrap-dotnet/HackerNewsService.cs b/bootstrap-dotnet/HackerNewsService.cs deleted file mode 100644 index 864c4ab9..00000000 --- a/bootstrap-dotnet/HackerNewsService.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Net.Http; -using System.Threading.Tasks; -using System.IO; - -public static class HackerNewsService -{ - private static readonly HttpClient client = new HttpClient(); - - public static UrlContentResponse GetUrlContent(GetUrlContentInput input) - { - try - { - var response = client.GetAsync(input.Url).GetAwaiter().GetResult(); - - if (!response.IsSuccessStatusCode) - { - return new UrlContentResponse - { - Supervisor = "If the error is retryable, try again. If not, tell the user why this failed.", - Message = $"Failed to fetch {input.Url}: {response.StatusCode}", - Response = response.ToString() - }; - } - - var html = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); - // Simple HTML tag stripping (except for tags) - var strippedHtml = System.Text.RegularExpressions.Regex.Replace( - html, - @"<(?!a\s|\/a\s|a>|\/a>)[^>]*>", - string.Empty - ); - - return new UrlContentResponse { Body = strippedHtml }; - } - catch (Exception ex) - { - return new UrlContentResponse { Error = ex.Message }; - } - } - - public static int ScoreHNPost(ScorePostInput input) - { - return input.Upvotes + (input.CommentCount * 2); - } - - public static GeneratePageResponse GeneratePage(GeneratePageInput input) - { - var html = $@" - - - Hacker News Page Generated by Inferable - - - -
{input.Markdown}
- - -"; - - var tmpPath = Path.Combine(Path.GetTempPath(), "inferable-hacker-news.html"); - File.WriteAllText(tmpPath, html); - - return new GeneratePageResponse - { - Message = "Tell the user to open the file at tmpPath in their browser.", - TmpPath = tmpPath - }; - } -} diff --git a/bootstrap-dotnet/Program.cs b/bootstrap-dotnet/Program.cs index 20931734..6c641e00 100644 --- a/bootstrap-dotnet/Program.cs +++ b/bootstrap-dotnet/Program.cs @@ -7,21 +7,17 @@ class Program { static async Task Main(string[] args) { - // Load environment variables in development if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development") { Env.Load(); } - // Setup dependency injection var services = new ServiceCollection(); - // Add logging services.AddLogging(builder => { builder.AddConsole(); }); - // Configure Inferable client services.AddSingleton(sp => { var options = new InferableOptions { ApiSecret = Environment.GetEnvironmentVariable("INFERABLE_API_SECRET"), @@ -35,21 +31,12 @@ static async Task Main(string[] args) var serviceProvider = services.BuildServiceProvider(); var client = serviceProvider.GetRequiredService(); - // Check command line arguments - if (args.Length > 0 && args[0].ToLower() == "run") - { - Console.WriteLine("Running HN extraction..."); - await RunHNExtraction.RunAsync(client); - } - else - { - Console.WriteLine("Starting client..."); - Register.RegisterFunctions(client); - await client.Default.StartAsync(); + Console.WriteLine("Starting client..."); + Register.RegisterFunctions(client); + await client.Default.StartAsync(); - Console.WriteLine("Press any key to exit..."); - Console.ReadKey(); - } + Console.WriteLine("Press any key to exit..."); + Console.ReadKey(); } } diff --git a/bootstrap-dotnet/README.md b/bootstrap-dotnet/README.md index 50409540..86bbf678 100644 --- a/bootstrap-dotnet/README.md +++ b/bootstrap-dotnet/README.md @@ -2,67 +2,109 @@

-# Inferable .NET Bootstrap +# .NET Bootstrap for Inferable -This is a .NET bootstrap application that demonstrates how to integrate and use our SDK. It serves as a reference implementation and starting point for .NET developers. +[![NuGet version](https://img.shields.io/nuget/v/Inferable.svg)](https://www.nuget.org/packages/Inferable/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Documentation](https://img.shields.io/badge/docs-inferable.ai-brightgreen)](https://docs.inferable.ai/) -## The Application +This is a bootstrap project demonstrating how to create an Inferable service in .NET. -The application is a simple .NET application that extracts the top posts from Hacker News and summarizes the comments for each post. It demonstrates how to: +## Installation -- Register C# functions with Inferable -- Trigger a Run programmatically to orchestrate the functions -- Control the control flow of the Run using native C# async/await primitives +1. Clone this repository +2. Install the Inferable NuGet package: -```mermaid -sequenceDiagram - participant extract - participant summarizePost - participant generatePage +```bash +dotnet add package Inferable +``` + +## Quick Start + +### Initializing the Client + +```csharp +using Inferable; - extract->>extract: Get top 3 HN posts - extract->>summarizePost: Posts data - summarizePost->>summarizePost: Get & analyze comments - summarizePost->>generatePage: Summaries data - generatePage->>generatePage: Generate HTML +var options = new InferableOptions +{ + ApiSecret = "your-api-secret", // Get yours at https://console.inferable.ai + BaseUrl = "https://api.inferable.ai" // Optional +}; + +var client = new InferableClient(options); ``` -## How to Run +If you don't provide an API key or base URL, it will attempt to read them from the following environment variables: -1. Start the local worker machine: +- `INFERABLE_API_SECRET` +- `INFERABLE_API_ENDPOINT` -```bash -dotnet run +### Registering the Exec Function + +This bootstrap demonstrates registering a secure command execution [function](https://docs.inferable.ai/pages/functions): + +```csharp +public class ExecInput +{ + [JsonPropertyName("command")] + public string Command { get; set; } // Only "ls" or "cat" allowed + + [JsonPropertyName("arg")] + public string Arg { get; set; } // Must start with "./" +} + +client.Default.RegisterFunction(new FunctionRegistration { + Name = "exec", + Description = "Executes a system command (only 'ls' and 'cat' are allowed)", + Func = new Func(ExecService.Exec), +}); + +await client.Default.StartAsync(); ``` -2. Trigger the HN extraction: +### Using the Function + +The exec function can be called through Inferable to execute safe system commands: ```bash -dotnet run run +ls ./src # Lists contents of ./src directory +cat ./README.md # Shows contents of README.md ``` -## How it works +The function returns: + +```csharp +public class ExecResponse +{ + public string Stdout { get; set; } // Command output + public string Stderr { get; set; } // Error output if any + public string Error { get; set; } // Exception message if failed +} +``` + +### Security Features + +This bootstrap implements several security measures: + +- Whitelist of allowed commands (`ls` and `cat` only) +- Path validation (only allows paths starting with `./`) +- Safe process execution settings +- Full error capture and reporting -1. The worker machine uses the Inferable .NET SDK to register functions with Inferable. These functions are registered in the `Register.cs` file: +## Documentation -- `GetUrlContent`: Fetches the content of a URL and strips HTML tags -- `ScoreHNPost`: Scores a Hacker News post based on upvotes and comment count -- `GeneratePage`: Generates an HTML page from markdown +- [Inferable Documentation](https://docs.inferable.ai/) +- [.NET SDK Documentation](https://docs.inferable.ai/dotnet) -2. The `Run.cs` script defines three main operations that are orchestrated by Inferable: +## Support -- `extract`: Extracts and scores the top 10 HN posts, selecting the top 3 -- `summarizePost`: Summarizes the comments for each selected post -- `generatePage`: Generates an HTML page containing the summaries +For support or questions, please [create an issue in the repository](https://github.com/inferablehq/inferable/issues). -3. The application flow: +## Contributing -- The extract operation fetches the top posts from Hacker News and scores them using internal scoring logic -- For each selected post, a separate summarization run analyzes the comments -- Finally, the generate operation creates an HTML page containing all the summaries +Contributions to the Inferable .NET Bootstrap are welcome. Please ensure that your code adheres to the existing style and includes appropriate tests. -4. The application uses C# primitives for control flow: +## License -- Async/await for non-blocking operations -- Strong typing with C# records and classes -- Reflection for inferring the function signatures and result schema +MIT diff --git a/bootstrap-dotnet/Register.cs b/bootstrap-dotnet/Register.cs index dec1185b..0c06df07 100644 --- a/bootstrap-dotnet/Register.cs +++ b/bootstrap-dotnet/Register.cs @@ -1,84 +1,36 @@ using Inferable; using System.Text.Json.Serialization; -// Input Models -public struct GetUrlContentInput -{ - [JsonPropertyName("url")] - public string Url { get; set; } -} - -public struct ScorePostInput -{ - [JsonPropertyName("commentCount")] - public int CommentCount { get; set; } - [JsonPropertyName("upvotes")] - public int Upvotes { get; set; } -} -public struct GeneratePageInput +public class ExecInput { - [JsonPropertyName("markdown")] - public string Markdown { get; set; } -} - -public struct EmptyInput -{ - [JsonPropertyName("noop")] - public string? Noop { get; set; } -} + [JsonPropertyName("command")] + public string Command { get; set; } -// Response Models -[JsonSerializable(typeof(UrlContentResponse))] -public class UrlContentResponse -{ - [JsonPropertyName("body")] - public string? Body { get; set; } - [JsonPropertyName("error")] - public string? Error { get; set; } - [JsonPropertyName("supervisor")] - public string? Supervisor { get; set; } - [JsonPropertyName("message")] - public string? Message { get; set; } - [JsonPropertyName("response")] - public string? Response { get; set; } + [JsonPropertyName("arg")] + public string Arg { get; set; } } -public class GeneratePageResponse +public class ExecResponse { - [JsonPropertyName("message")] - public string? Message { get; set; } + [JsonPropertyName("stdout")] + public string Stdout { get; set; } - [JsonPropertyName("tmpPath")] - public string? TmpPath { get; set; } -} + [JsonPropertyName("stderr")] + public string Stderr { get; set; } -public class ScorePostResponse -{ - [JsonPropertyName("score")] - public int Score { get; set; } + [JsonPropertyName("error")] + public string Error { get; set; } } public static class Register { public static void RegisterFunctions(InferableClient client) { - client.Default.RegisterFunction(new FunctionRegistration { - Name = "getUrlContent", - Description = "Gets the content of a URL", - Func = new Func(HackerNewsService.GetUrlContent), - }); - - client.Default.RegisterFunction(new FunctionRegistration { - Name = "scoreHNPost", - Description = "Calculates a score for a Hacker News post given its comment count and upvotes", - Func = new Func(input => HackerNewsService.ScoreHNPost(input)) - }); - - client.Default.RegisterFunction(new FunctionRegistration { - Name = "generatePage", - Description = "Generates a page from markdown", - Func = new Func(HackerNewsService.GeneratePage) + client.Default.RegisterFunction(new FunctionRegistration { + Name = "exec", + Description = "Executes a system command (only 'ls' and 'cat' are allowed)", + Func = new Func(ExecService.Exec), }); } } diff --git a/bootstrap-dotnet/Run.cs b/bootstrap-dotnet/Run.cs deleted file mode 100644 index e938ce38..00000000 --- a/bootstrap-dotnet/Run.cs +++ /dev/null @@ -1,176 +0,0 @@ -using System.Diagnostics; -using Inferable; -using System.Text.Json; -using Inferable.API; -using NJsonSchema; -using System.Text.Json.Serialization; -using System.Collections.ObjectModel; - -public class Post -{ - [JsonPropertyName("id")] - public string Id { get; set; } = ""; - - [JsonPropertyName("title")] - public string Title { get; set; } = ""; - - [JsonPropertyName("points")] - public string Points { get; set; } = ""; - - [JsonPropertyName("comments_url")] - public string CommentsUrl { get; set; } = ""; -} - -public class KeyPoint -{ - [JsonPropertyName("id")] - public string Id { get; set; } = ""; - - [JsonPropertyName("title")] - public string Title { get; set; } = ""; - - [JsonPropertyName("key_points")] - public List KeyPoints { get; set; } = new(); -} - -public class GeneratePageResult -{ - [JsonPropertyName("page_path")] - public string PagePath { get; set; } = ""; -} - -public class ExtractResult -{ - [JsonPropertyName("posts")] - public Collection Posts { get; set; } = new(); -} - - -public static class RunHNExtraction -{ - private static void OpenInferableInBrowser() - { - try - { - var clusterId = Environment.GetEnvironmentVariable("INFERABLE_CLUSTER_ID"); - var url = $"https://app.inferable.ai/clusters"; - - if (!string.IsNullOrEmpty(clusterId)) - { - url += $"/{clusterId}/runs"; - } - - if (OperatingSystem.IsWindows()) - { - Process.Start(new ProcessStartInfo("cmd", $"/c start {url}") { CreateNoWindow = true }); - } - else if (OperatingSystem.IsMacOS()) - { - Process.Start("open", url); - } - else if (OperatingSystem.IsLinux()) - { - Process.Start("xdg-open", url); - } - } - catch (Exception ex) - { - Console.WriteLine($"Failed to open browser: {ex.Message}"); - } - } - - public static async Task RunAsync(InferableClient? client = null) - { - // Use the provided client or create a new one if none is provided - client ??= new InferableClient(new InferableOptions - { - ApiSecret = Environment.GetEnvironmentVariable("INFERABLE_API_SECRET"), - BaseUrl = Environment.GetEnvironmentVariable("INFERABLE_API_ENDPOINT") - }); - - OpenInferableInBrowser(); - - // Extract top posts - var extractRun = await client.CreateRunAsync(new Inferable.API.CreateRunInput - { - InitialPrompt = @" - Hacker News has a homepage at https://news.ycombinator.com/ - Each post has a id, title, a link, and a score, and is voted on by users. - Score the top 10 posts and pick the top 3 according to the internal scoring function.", - CallSummarization = false, - ResultSchema = JsonSchema.FromType() - }); - - var extractResult = await extractRun.PollAsync(null); - - if (extractResult.GetValueOrDefault().Result == null) - { - throw new Exception("No result found in extract run"); - } - - var posts = JsonSerializer.Deserialize(extractResult.GetValueOrDefault().Result?.ToString()!); - - if (posts?.Posts == null) - { - throw new Exception("No posts found in extract result"); - } - - // Summarize each post - var summaryTasks = new List>(); - foreach (var post in posts.Posts) - { - var summarizeRun = await client.CreateRunAsync(new CreateRunInput - { - InitialPrompt = $@" - - {JsonSerializer.Serialize(post)} - - - You are given a post from Hacker News, and a url for the post's comments. - Summarize the comments. You should visit the comments URL to get the comments. - Produce a list of the key points from the comments.", - ResultSchema = JsonSchema.FromType() - }); - - summaryTasks.Add(summarizeRun.PollAsync(null)); - } - - // Wait for all summaries to complete - var summaryResults = await Task.WhenAll(summaryTasks); - var summaries = new List(); - - foreach (var result in summaryResults) - { - if (result?.Result != null) - { - var summary = JsonSerializer.Deserialize(result.GetValueOrDefault().Result?.ToString()!); - if (summary != null) - { - summaries.Add(summary); - } - } - } - - // Generate final page - var generateRun = await client.CreateRunAsync(new CreateRunInput - { - InitialPrompt = $@" - - {JsonSerializer.Serialize(summaries)} - - - You are given a list of posts from Hacker News, and a summary of the comments for each post. - - Generate a web page with the following structure: - - A header with the title of the page - - A list of posts, with the title, a link to the post, and the key points from the comments in a ul - - A footer with a link to the original Hacker News page", - ResultSchema = JsonSchema.FromType() - }); - - var generateResult = await generateRun.PollAsync(null); - var pageResult = JsonSerializer.Deserialize(generateResult.GetValueOrDefault().Result?.ToString()!); - - Console.WriteLine($"Generated page: {JsonSerializer.Serialize(pageResult)}"); - } -} diff --git a/bootstrap-dotnet/Trigger.cs b/bootstrap-dotnet/Trigger.cs new file mode 100644 index 00000000..550bb7c2 --- /dev/null +++ b/bootstrap-dotnet/Trigger.cs @@ -0,0 +1,45 @@ +using System.Text.Json; +using Inferable; +using Inferable.API; +using NJsonSchema; +using System.Text.Json.Serialization; + +public class Report +{ + [JsonPropertyName("name")] + public string Name { get; set; } = ""; + + [JsonPropertyName("capabilities")] + public List Capabilities { get; set; } = new(); +} + +public static class RunSourceInspection +{ + public static async Task RunAsync(InferableClient? client = null) + { + // Use the provided client or create a new one if none is provided + client ??= new InferableClient(new InferableOptions + { + ApiSecret = Environment.GetEnvironmentVariable("INFERABLE_API_SECRET"), + BaseUrl = Environment.GetEnvironmentVariable("INFERABLE_API_ENDPOINT") + }); + + var run = await client.CreateRunAsync(new CreateRunInput + { + InitialPrompt = @" + Iteratively inspect the source code at the current directory, and produce a report. + You may selectively inspect the contents of files. You can only access files starting with ""./""", + ResultSchema = JsonSchema.FromType() + }); + + var result = await run.PollAsync(null); + + if (result?.Result == null) + { + throw new Exception("No result found in run"); + } + + var report = JsonSerializer.Deserialize(result.Result.ToString()!); + Console.WriteLine($"Report: {JsonSerializer.Serialize(report)}"); + } +} diff --git a/bootstrap-go/README.md b/bootstrap-go/README.md index 19df2ff3..0414ac41 100644 --- a/bootstrap-go/README.md +++ b/bootstrap-go/README.md @@ -6,25 +6,32 @@ This is a Go bootstrap application that demonstrates how to integrate and use our SDK. It serves as a reference implementation and starting point for Go developers. -## The Application +## Docs -The application is a simple Go application that extracts the top posts from Hacker News and summarizes the comments for each post. It demonstrates how to: +To follow along with the docs, go to our [quickstart](https://docs.inferable.ai/quick-start). -- Register Go functions with Inferable -- Trigger a Run programmatically to orchestrate the functions -- Control the control flow of the Run using native Go control flow primitives +## What does this application do? + +The application demonstrates an agent that can inspect and analyze source code by iteratively executing system commands. It shows how to: + +1. Register Go functions with Inferable ([main.go](./main.go)) +2. Trigger a Run programmatically to provide a goal ([cmd/trigger.go](./cmd/trigger.go)) ```mermaid sequenceDiagram - participant extract - participant summarizePost - participant generatePage - - extract->>extract: Get top 3 HN posts - extract->>summarizePost: Posts data - summarizePost->>summarizePost: Get & analyze comments - summarizePost->>generatePage: Summaries data - generatePage->>generatePage: Generate HTML + participant Agent + participant exec + participant FileSystem + + Agent->>exec: Request file listing (ls) + exec->>FileSystem: Execute ls command + FileSystem->>exec: Return file list + exec->>Agent: File list results + Agent->>exec: Request file contents (cat) + exec->>FileSystem: Execute cat command + FileSystem->>exec: Return file contents + exec->>Agent: File contents + Agent->>Agent: Analyze code and generate report ``` ## How to Run @@ -38,25 +45,34 @@ go run main.go 2. Trigger the Run ```bash -go run cmd/run.go +go run cmd/trigger.go ``` ## How it works -1. The worker machine uses the Inferable Go SDK to register the functions with Inferable. These functions are: +1. The worker machine uses the Inferable Go SDK to register the `exec` function with Inferable. This function: + + - Accepts `ls` or `cat` commands with path arguments + - Only allows accessing paths that start with "./" + - Returns the stdout and stderr from the command execution + +2. The `trigger.go` script creates a Re-Act agent that: -- `GetUrlContent`: Get the html content of any url -- `ScoreHNPost`: Score a post based on the number of comments and upvotes -- `GeneratePage`: Generate an HTML page with the summaries and save it to a tmp file in your OS's temp directory + - Receives an initial prompt to inspect source code in the current directory + - Can iteratively call the `exec` function to list and read files + - Produces a final report containing: + - The name of the program + - A list of its capabilities -2. The `run.go` script defines "Runs" with the Inferable Go SDK. These are: +3. The agent will: -- `extractRun`: Extracts the top 3 HN posts -- `summarizeRun`: Summarizes the comments for a given post -- `generateRun`: Generates an HTML page from the summaries + - Use `ls` to discover files in the directory + - Use `cat` to read the contents of relevant files + - Analyze the code to understand its functionality + - Generate a structured report based on its findings -3. Given the run configuration (prompts, result schema, etc), the worker machine will orchestrate the functions to generate the page. +## Security -- `extractRun` will get the top 3 HN posts using the `GetUrlContent` function, and score them using the `ScoreHNPost` function -- `summarizeRun` will summarize the comments for each post using the `GetUrlContent` function -- `generateRun` will generate the HTML page using the `GeneratePage` function +- The `exec` function is restricted to only allow `ls` and `cat` commands +- File access is restricted to paths starting with "./" +- The constraints are enforced by source code, and cannot be bypassed by the agent diff --git a/bootstrap-go/cmd/run.go b/bootstrap-go/cmd/run.go deleted file mode 100644 index e676783a..00000000 --- a/bootstrap-go/cmd/run.go +++ /dev/null @@ -1,213 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "os" - "os/exec" - - inferable "github.com/inferablehq/inferable/sdk-go" - "github.com/joho/godotenv" -) - -type Post struct { - ID string `json:"id"` - Title string `json:"title"` - Points string `json:"points"` - CommentsURL string `json:"commentsUrl"` -} - -type ExtractResult struct { - Posts []Post `json:"posts"` -} - -type KeyPoint struct { - ID string `json:"id"` - Title string `json:"title"` - KeyPoints []string `json:"keyPoints"` -} - -type GeneratePageResult struct { - PagePath string `json:"pagePath"` -} - -type SummarizeResult struct { - Index int - Summary KeyPoint - Error error -} - -func main() { - if err := godotenv.Load(); err != nil { - fmt.Printf("Warning: Error loading .env file: %v\n", err) - } - - client, err := inferable.New(inferable.InferableOptions{ - APISecret: os.Getenv("INFERABLE_API_SECRET"), - APIEndpoint: os.Getenv("INFERABLE_API_ENDPOINT"), - }) - if err != nil { - panic(err) - } - - fmt.Println("Opening browser to view runs") - - url := "https://app.inferable.ai/clusters" - - if os.Getenv("INFERABLE_CLUSTER_ID") != "" { - url = fmt.Sprintf("https://app.inferable.ai/clusters/%s/runs", os.Getenv("INFERABLE_CLUSTER_ID")) - } - - cmd := exec.Command("open", url) - if err := cmd.Run(); err != nil { - fmt.Printf("Failed to open browser: %v\n", err) - } - - // 1. Extract top posts from Hacker News - extractionSchema := map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "posts": map[string]interface{}{ - "type": "array", - "items": map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "id": map[string]string{"type": "string"}, - "title": map[string]string{"type": "string"}, - "points": map[string]string{"type": "string"}, - "commentsUrl": map[string]string{"type": "string"}, - }, - }, - }, - }, - } - - extractRun, err := client.CreateRun(inferable.CreateRunInput{ - InitialPrompt: `Hacker News has a homepage at https://news.ycombinator.com/ - Each post has a id, title, a link, and a score, and is voted on by users. - Score the top 10 posts and pick the top 3 according to the internal scoring function.`, - CallSummarization: false, - ResultSchema: extractionSchema, - }) - if err != nil { - panic(err) - } - - extractResult, err := extractRun.Poll(nil) - if err != nil { - panic(err) - } - - var posts ExtractResult - resultBytes, err := json.Marshal(extractResult.Result) - if err != nil { - panic(fmt.Sprintf("failed to marshal extract result: %v", err)) - } - if err := json.Unmarshal(resultBytes, &posts); err != nil { - panic(err) - } - - // 2. Summarize comments for each post concurrently - summarizationSchema := map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "id": map[string]string{"type": "string"}, - "title": map[string]string{"type": "string"}, - "keyPoints": map[string]string{"type": "array"}, - }, - } - - summaryChan := make(chan SummarizeResult, len(posts.Posts)) - - for i, post := range posts.Posts { - go func(index int, post Post) { - summarizeRun, err := client.CreateRun(inferable.CreateRunInput{ - InitialPrompt: fmt.Sprintf(` - - %s - - - You are given a post from Hacker News, and a url for the post's comments. - Summarize the comments. You should visit the comments URL to get the comments. - Produce a list of the key points from the comments. - `, post), - ResultSchema: summarizationSchema, - }) - if err != nil { - summaryChan <- SummarizeResult{Index: index, Error: err} - return - } - - summarizeResult, err := summarizeRun.Poll(nil) - if err != nil { - summaryChan <- SummarizeResult{Index: index, Error: err} - return - } - - var summary KeyPoint - resultBytes, err := json.Marshal(summarizeResult.Result) - if err != nil { - summaryChan <- SummarizeResult{Index: index, Error: fmt.Errorf("failed to marshal summarize result: %v", err)} - return - } - if err := json.Unmarshal(resultBytes, &summary); err != nil { - summaryChan <- SummarizeResult{Index: index, Error: err} - return - } - - summaryChan <- SummarizeResult{Index: index, Summary: summary} - }(i, post) - } - - summaries := make([]KeyPoint, len(posts.Posts)) - for range posts.Posts { - result := <-summaryChan - if result.Error != nil { - panic(result.Error) - } - summaries[result.Index] = result.Summary - } - - // 3. Generate final HTML page with summaries - pageGenerationSchema := map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "pagePath": map[string]string{"type": "string"}, - }, - } - - generateRun, err := client.CreateRun(inferable.CreateRunInput{ - InitialPrompt: fmt.Sprintf(` - - %s - - - You are given a list of posts from Hacker News, and a summary of the comments for each post. - - Generate markdown with the following structure, and generate an HTML page from it. - - A header with the title of the page - - A list of posts, with the title, a link to the post, and the key points from the comments in a ul - - A footer with a link to the original Hacker News page - `, summaries), - ResultSchema: pageGenerationSchema, - }) - if err != nil { - panic(err) - } - - generateResult, err := generateRun.Poll(nil) - if err != nil { - panic(err) - } - - var pageResult GeneratePageResult - resultBytes, err = json.Marshal(generateResult.Result) - if err != nil { - panic(fmt.Sprintf("failed to marshal generate result: %v", err)) - } - if err := json.Unmarshal(resultBytes, &pageResult); err != nil { - panic(err) - } - - fmt.Printf("Generated page: %+v\n", pageResult) -} diff --git a/bootstrap-go/cmd/trigger.go b/bootstrap-go/cmd/trigger.go new file mode 100644 index 00000000..111fc688 --- /dev/null +++ b/bootstrap-go/cmd/trigger.go @@ -0,0 +1,68 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + + inferable "github.com/inferablehq/inferable/sdk-go" + "github.com/joho/godotenv" +) + +type Report struct { + Name string `json:"name"` + Capabilities []string `json:"capabilities"` +} + +func main() { + if err := godotenv.Load(); err != nil { + fmt.Printf("Warning: Error loading .env file: %v\n", err) + } + + client, err := inferable.New(inferable.InferableOptions{ + APISecret: os.Getenv("INFERABLE_API_SECRET"), + APIEndpoint: os.Getenv("INFERABLE_API_ENDPOINT"), + }) + if err != nil { + panic(err) + } + + // Define the schema for the report + reportSchema := map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "name": map[string]string{"type": "string"}, + "capabilities": map[string]interface{}{ + "type": "array", + "items": map[string]string{"type": "string"}, + "description": "The capabilities of the program. What it can do.", + }, + }, + } + + // Create and execute the run + run, err := client.CreateRun(inferable.CreateRunInput{ + InitialPrompt: `Iteratively inspect the source code at the current directory, and produce a report. + You may selectively inspect the contents of files. You can only access files starting with "./"`, + ResultSchema: reportSchema, + }) + if err != nil { + panic(err) + } + + result, err := run.Poll(nil) + if err != nil { + panic(err) + } + + var report Report + resultBytes, err := json.Marshal(result.Result) + if err != nil { + panic(fmt.Sprintf("failed to marshal result: %v", err)) + } + if err := json.Unmarshal(resultBytes, &report); err != nil { + panic(err) + } + + fmt.Printf("%+v\n", report) +} diff --git a/bootstrap-go/functions.go b/bootstrap-go/functions.go deleted file mode 100644 index 25b3191e..00000000 --- a/bootstrap-go/functions.go +++ /dev/null @@ -1,102 +0,0 @@ -package main - -import ( - "fmt" - "io" - "net/http" - "os" - "path" - "strings" -) - -func GetUrlContent(input struct { - URL string `json:"url"` -}) (interface{}, error) { - resp, err := http.Get(input.URL) - if err != nil { - return map[string]interface{}{ - "supervisor": "If the error is retryable, try again. If not, tell the user why this failed.", - "message": fmt.Sprintf("Failed to fetch %s: %v", input.URL, err), - "response": nil, - }, nil - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return map[string]interface{}{ - "supervisor": "If the error is retryable, try again. If not, tell the user why this failed.", - "message": fmt.Sprintf("Failed to fetch %s: %s", input.URL, resp.Status), - "response": resp, - }, nil - } - - html, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - cleaned := removeHTMLTags(string(html)) - - return map[string]interface{}{ - "body": cleaned, - }, nil -} - -func ScoreHNPost(input struct { - CommentCount int `json:"commentCount"` - Upvotes int `json:"upvotes"` -}) (interface{}, error) { - score := input.Upvotes + input.CommentCount*2 - return score, nil -} - -func GeneratePage(input struct { - Markdown string `json:"markdown"` -}) (interface{}, error) { - html := fmt.Sprintf(` - - - Hacker News Page Generated by Inferable - - - -
%s
- - - - `, input.Markdown) - - tmpDir := os.TempDir() - tmpPath := path.Join(tmpDir, "inferable-hacker-news.html") - - err := os.WriteFile(tmpPath, []byte(html), 0644) - if err != nil { - return nil, err - } - - return map[string]interface{}{ - "message": "Tell the user to open the file at tmpPath in their browser.", - "tmpPath": tmpPath, - }, nil -} - -// Helper function to remove HTML tags except for
tags -func removeHTMLTags(html string) string { - parts := strings.Split(html, "<") - result := []string{parts[0]} - - for _, part := range parts[1:] { - if strings.HasPrefix(part, "a ") || strings.HasPrefix(part, "/a>") || strings.HasPrefix(part, "a>") { - result = append(result, "<"+part) - } else { - if idx := strings.Index(part, ">"); idx != -1 { - result = append(result, part[idx+1:]) - } - } - } - - return strings.Join(result, "") -} diff --git a/bootstrap-go/main.go b/bootstrap-go/main.go index fef550a4..137e73f1 100644 --- a/bootstrap-go/main.go +++ b/bootstrap-go/main.go @@ -3,11 +3,60 @@ package main import ( "fmt" "os" + "os/exec" + "strings" inferable "github.com/inferablehq/inferable/sdk-go" "github.com/joho/godotenv" ) +type ExecOutput struct { + Stdout string `json:"stdout"` + Stderr string `json:"stderr"` + Error string `json:"error"` +} + +func Exec(input struct { + Command string `json:"command"` + Arg string `json:"arg"` +}) ExecOutput { + allowedCommands := map[string]bool{ + "ls": true, + "cat": true, + } + if !allowedCommands[input.Command] { + return ExecOutput{ + Error: fmt.Sprintf("command not allowed: %s. Allowed commands: %v", input.Command, allowedCommands), + } + } + + // Only allow access to files starting with ./ + if !strings.HasPrefix(input.Arg, "./") { + return ExecOutput{ + Error: "requires arg starting with './'. Example: 'ls ./'", + } + } + + cmd := exec.Command(input.Command, input.Arg) + stdout, err := cmd.Output() + if err != nil { + var stderr string + if exitErr, ok := err.(*exec.ExitError); ok { + stderr = string(exitErr.Stderr) + } + return ExecOutput{ + Stdout: "", + Stderr: stderr, + Error: fmt.Sprintf("command failed: %s", input.Command), + } + } + + return ExecOutput{ + Stdout: strings.TrimSpace(string(stdout)), + Stderr: "", + } +} + func main() { // Load vars from .env file err := godotenv.Load() @@ -15,10 +64,8 @@ func main() { panic(err) } - // Instantiate the Inferable client. + // Instantiate the Inferable client client, err := inferable.New(inferable.InferableOptions{ - // To get a new key, run: - // npx @inferable/cli auth keys create 'My New Machine Key' --type='cluster_machine' APISecret: os.Getenv("INFERABLE_API_SECRET"), APIEndpoint: os.Getenv("INFERABLE_API_ENDPOINT"), }) @@ -27,29 +74,11 @@ func main() { panic(err) } - // Register functions that match the Node.js implementation + // Register the exec function _, err = client.Default.RegisterFunc(inferable.Function{ - Func: GetUrlContent, - Name: "getUrlContent", - Description: "Gets the content of a URL", - }) - if err != nil { - panic(err) - } - - _, err = client.Default.RegisterFunc(inferable.Function{ - Func: GeneratePage, - Name: "generatePage", - Description: "Generates a page from markdown", - }) - if err != nil { - panic(err) - } - - _, err = client.Default.RegisterFunc(inferable.Function{ - Func: ScoreHNPost, - Name: "scoreHNPost", - Description: "Calculates a score for a Hacker News post given its comment count and upvotes", + Func: Exec, + Name: "exec", + Description: "Executes a system command", }) if err != nil { panic(err) @@ -61,7 +90,6 @@ func main() { } fmt.Println("Inferable service started") - fmt.Println("Press CTRL+C to stop") // Wait for CTRL+C diff --git a/bootstrap-node/README.md b/bootstrap-node/README.md index d136c696..8f5b23fe 100644 --- a/bootstrap-node/README.md +++ b/bootstrap-node/README.md @@ -6,13 +6,16 @@ This is a Node.js bootstrap application that demonstrates how to integrate and use our SDK. It serves as a reference implementation and starting point for Node.js developers. -## The Application +## Docs + +To follow along with the docs, go to our [quickstart](https://docs.inferable.ai/quick-start). + +## What does this application do? The application demonstrates an agent that can inspect and analyze source code by iteratively executing system commands. It shows how to: -- Register Typescript functions with Inferable -- Trigger a Run programmatically to provide a goal -- Restrict the agent's access to the filesystem using source code +1. Register Typescript functions with Inferable ([index.ts](./src/index.ts)) +2. Trigger a Run programmatically to provide a goal ([run.ts](./src/run.ts)) ```mermaid sequenceDiagram @@ -42,7 +45,7 @@ npm run dev 2. Trigger the Run ```bash -npm run run +npm run trigger ``` ## How it works diff --git a/bootstrap-node/package.json b/bootstrap-node/package.json index 25b19f5f..60b65e5a 100644 --- a/bootstrap-node/package.json +++ b/bootstrap-node/package.json @@ -3,7 +3,7 @@ "main": "src/index.ts", "scripts": { "dev": "tsx -r dotenv/config --watch src/index.ts", - "run": "tsx -r dotenv/config src/run.ts" + "trigger": "tsx -r dotenv/config src/trigger.ts" }, "engines": { "node": ">=20" @@ -17,4 +17,4 @@ "devDependencies": { "@types/node": "^22.10.1" } -} +} \ No newline at end of file diff --git a/bootstrap-node/src/run.ts b/bootstrap-node/src/trigger.ts similarity index 94% rename from bootstrap-node/src/run.ts rename to bootstrap-node/src/trigger.ts index 46010b3d..d62d36fb 100644 --- a/bootstrap-node/src/run.ts +++ b/bootstrap-node/src/trigger.ts @@ -16,7 +16,7 @@ client .run({ initialPrompt: ` Iteratively inspect the source code at the current directory, and produce a report. - You may selectively inspect the contents of files. You can only access files starting with ./ + You may selectively inspect the contents of files. You can only access files starting with "./" `.trim(), resultSchema: reportSchema, }) From 7816fec6e2cc727f4f84463f3fe3e6d0e92312b7 Mon Sep 17 00:00:00 2001 From: Nadeesha Cabral Date: Mon, 9 Dec 2024 20:54:55 +1100 Subject: [PATCH 5/5] update --- bootstrap-dotnet/ExecService.cs | 18 +++++++++--------- bootstrap-dotnet/Program.cs | 8 ++++++++ bootstrap-dotnet/Register.cs | 23 ----------------------- bootstrap-dotnet/Trigger.cs | 2 +- 4 files changed, 18 insertions(+), 33 deletions(-) diff --git a/bootstrap-dotnet/ExecService.cs b/bootstrap-dotnet/ExecService.cs index 523e35eb..3e6e3954 100644 --- a/bootstrap-dotnet/ExecService.cs +++ b/bootstrap-dotnet/ExecService.cs @@ -42,9 +42,9 @@ public static ExecResponse Exec(ExecInput input) }; using var process = Process.Start(startInfo); - var stdout = process.StandardOutput.ReadToEnd().Trim(); - var stderr = process.StandardError.ReadToEnd().Trim(); - process.WaitForExit(); + var stdout = process?.StandardOutput.ReadToEnd().Trim(); + var stderr = process?.StandardError.ReadToEnd().Trim(); + process?.WaitForExit(); return new ExecResponse { @@ -62,15 +62,15 @@ public static ExecResponse Exec(ExecInput input) } } -public class ExecInput +public struct ExecInput { - public string Command { get; set; } - public string Arg { get; set; } + public required string Command { get; set; } + public required string Arg { get; set; } } public class ExecResponse { - public string Stdout { get; set; } - public string Stderr { get; set; } - public string Error { get; set; } + public string? Stdout { get; set; } + public string? Stderr { get; set; } + public string? Error { get; set; } } diff --git a/bootstrap-dotnet/Program.cs b/bootstrap-dotnet/Program.cs index 6c641e00..cf56d234 100644 --- a/bootstrap-dotnet/Program.cs +++ b/bootstrap-dotnet/Program.cs @@ -31,6 +31,14 @@ static async Task Main(string[] args) var serviceProvider = services.BuildServiceProvider(); var client = serviceProvider.GetRequiredService(); + // Check if "trigger" command was passed + if (args.Length > 0 && args[0].ToLower() == "trigger") + { + await RunSourceInspection.RunAsync(client); + return; + } + + // Default behavior - run the service Console.WriteLine("Starting client..."); Register.RegisterFunctions(client); await client.Default.StartAsync(); diff --git a/bootstrap-dotnet/Register.cs b/bootstrap-dotnet/Register.cs index 0c06df07..6ab7715c 100644 --- a/bootstrap-dotnet/Register.cs +++ b/bootstrap-dotnet/Register.cs @@ -1,27 +1,4 @@ using Inferable; -using System.Text.Json.Serialization; - - -public class ExecInput -{ - [JsonPropertyName("command")] - public string Command { get; set; } - - [JsonPropertyName("arg")] - public string Arg { get; set; } -} - -public class ExecResponse -{ - [JsonPropertyName("stdout")] - public string Stdout { get; set; } - - [JsonPropertyName("stderr")] - public string Stderr { get; set; } - - [JsonPropertyName("error")] - public string Error { get; set; } -} public static class Register { diff --git a/bootstrap-dotnet/Trigger.cs b/bootstrap-dotnet/Trigger.cs index 550bb7c2..71395b6f 100644 --- a/bootstrap-dotnet/Trigger.cs +++ b/bootstrap-dotnet/Trigger.cs @@ -39,7 +39,7 @@ public static async Task RunAsync(InferableClient? client = null) throw new Exception("No result found in run"); } - var report = JsonSerializer.Deserialize(result.Result.ToString()!); + var report = JsonSerializer.Deserialize(result?.Result?.ToString() ?? ""); Console.WriteLine($"Report: {JsonSerializer.Serialize(report)}"); } }