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)}");
}
}