Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/mighty-boats-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@knocklabs/agent-toolkit": patch
---

fix: pre-launch fixes
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ The Knock Agent toolkit enables popular agent frameworks including [OpenAI](http

Using the Knock agent toolkit allows you to build powerful agent systems that are capable of sending cross-channel notifications to the humans who need to be in the loop. As a developer, it also helps you build Knock integrations and manage your Knock account.

You can [read more in the documentation](https://docs.knock.app/developer-tools/agent-toolkit/overview).

## API reference

The Knock Agent Toolkit provides three main entry points:
The Knock Agent Toolkit provides four main entry points:

- `@knocklabs/agent-toolkit/ai-sdk`: Helpers for integrating with Vercel's AI SDK.
- `@knocklabs/agent-toolkit/ai-sdk`: Helpers for integrating with [Vercel's AI SDK](https://sdk.vercel.ai/).
- `@knocklabs/agent-tookkit/langchain`: Helpers for integrating with [Langchain's JS SDK](https://github.com/langchain-ai/langchainjs).
- `@knocklabs/agent-toolkit/openai`: Helpers for integrating with the OpenAI SDK.
- `@knocklabs/agent-toolkit/openai`: Helpers for integrating with the [OpenAI SDK](https://platform.openai.com/docs/guides/function-calling?api-mode=chat&lang=javascript).
- `@knocklabs/agent-toolkit/modelcontextprotocol`: Low level helpers for integrating with the Model Context Protocol (MCP).

## Prerequisites
Expand Down
16 changes: 9 additions & 7 deletions examples/ai-sdk/hitl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,29 @@ dotenv.config();
});

const addTool = tool({
description: "Add two numbers together. ALWAYS use this tool when you are asked to do addition DO NOT assume the result.",
description: "Add two numbers together.",
parameters: z.object({
a: z.number(),
b: z.number(),
}),
// This will never be called because we're deferring the tool call to the human in the loop workflow
execute: async ({ a, b }) => {
console.log("Executing add tool");
return a + b;
},
});

// This will defer the tool call and trigger a human in the loop workflow
const toolset = toolkit.requireHumanInput({ add: addTool }, {
workflow: "approve-tool-call",
recipients: ["admin_user_1"],
});
const { add: addToolWithApproval } = toolkit.requireHumanInput(
{ add: addTool },
{
workflow: "approve-tool-call",
recipients: ["admin_user_1"],
}
);

const result = await generateText({
model: openai("gpt-4o"),
tools: { ...toolset },
tools: { add: addToolWithApproval },
maxSteps: 5,
prompt: "Add 1 and 2 together.",
});
Expand Down
36 changes: 28 additions & 8 deletions examples/ai-sdk/index.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,48 @@
import { openai } from "@ai-sdk/openai";
import { generateText } from "ai";
import { generateText, tool } from "ai";
import { createKnockToolkit } from "@knocklabs/agent-toolkit/ai-sdk";
import dotenv from "dotenv";
import { z } from "zod";

dotenv.config();

const mockCard = {
last4: "1234",
brand: "Visa",
expiration: "01/2028",
holder_name: "Alan Grant",
card_url: "https://fintech.com/cards/1234",
};

const mockIssueCardTool = tool({
description: "Issue a card to a user",
parameters: z.object({ userId: z.string() }),
execute: async (_params) => ({ card: mockCard }),
});

(async () => {
const toolkit = await createKnockToolkit({
serviceToken: process.env.KNOCK_SERVICE_TOKEN!,
permissions: {
users: { manage: true },
// Expose the card-issued workflow as a tool
workflows: { trigger: ["card-issued"] },
},
userId: "alan-grant",
});

const result = await generateText({
model: openai("gpt-4o"),
tools: {
...toolkit.getTools("users"),
...toolkit.getAllTools(),
issueCard: mockIssueCardTool,
},
system:
"You are a friendly assistant that helps with card issuing. When a user is issued a card you should trigger the card-issued workflow with the recipient as the user who the card is issued to.",
prompt: "I'd like to issue a card to Alan Grant (id: alan-grant).",
maxSteps: 5,
prompt:
"Update the current user's profile with information about them, knowing that they are Alan Grant from Jurassic Park. Include custom properties about their favorite dinosaur.",
});

console.log(result);

for (const message of result.response.messages) {
console.log(message.content);
}
})();
2 changes: 1 addition & 1 deletion examples/ai-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "NODE_OPTIONS='--loader ts-node/esm --experimental-specifier-resolution=node' node hitl.ts"
"dev": "NODE_OPTIONS='--loader ts-node/esm --experimental-specifier-resolution=node' node index.ts"
},
"author": "",
"license": "MIT",
Expand Down
10 changes: 4 additions & 6 deletions src/ai-sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ type KnockToolkit = {
*
* You can filter the set of tools that are available by setting the `config.permissions` property.
*
* When the `config.permissions.workflows.run` is set, then workflow triggers for
* When the `config.permissions.workflows.trigger` is set, then workflow triggers for
* the specified workflows will be included in the returned tools.
*
* You can also specify a list of workflow keys to include in the returned tools, should you wish to
Expand Down Expand Up @@ -70,8 +70,7 @@ const createKnockToolkit = async (

return Promise.resolve({
/**
* Get all tools for all categories. When the `config.permissions.workflows.run` is set, then workflow triggers for
* the specified workflows will be included in the returned tools.
* Get all tools for all categories, including workflow triggers when workflows-as-tools are enabled.
*
* @returns A promise that resolves to a set of tools
*/
Expand All @@ -80,7 +79,7 @@ const createKnockToolkit = async (
},

/**
* Get all tools for a specific category. When trying to get tools for the `workflows` category and the run permission is set,
* Get all tools for a specific category. When trying to get tools for the `workflows` category and the trigger permission is set,
* the workflow triggers for the specified workflows will be included in the returned tools.
*
* @param category - The category of tools to get
Expand All @@ -95,8 +94,7 @@ const createKnockToolkit = async (
},

/**
* Get a map of all tools by method name. When the `config.permissions.workflows.run` is set, then workflow triggers for
* the specified workflows will be included in the returned tools.
* Get a map of all tools by method name, including workflow triggers when workflows-as-tools are enabled.
*
* @returns A map of all tools by method name
*/
Expand Down
13 changes: 6 additions & 7 deletions src/lib/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ vi.mock("../tools/index.js", () => ({
},
workflows: {
read: ["listWorkflows"],
run: ["triggerWorkflow"],
manage: ["triggerWorkflow"],
trigger: [],
},
},
}));
Expand Down Expand Up @@ -207,21 +208,19 @@ describe("utils", () => {
expect(result.users).toHaveLength(0);
});

it("should return workflow tools when run permission is granted", async () => {
it("should return workflow tools when trigger permission is granted", async () => {
const config: ToolkitConfig = {
serviceToken: "test",
permissions: { workflows: { run: true } },
permissions: { workflows: { trigger: ["test"] } },
};

const result = await getToolsByPermissionsInCategories(
knockClient,
config
);

expect(result.workflows).toHaveLength(2);

expect(result.workflows[0].method).toBe("trigger_workflow");
expect(result.workflows[1].method).toBe("trigger_test_workflow");
expect(result.workflows).toHaveLength(1);
expect(result.workflows[0].method).toBe("trigger_test_workflow");
});
});

Expand Down
20 changes: 14 additions & 6 deletions src/lib/knock-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,16 @@ const serviceTokensToApiClients: Record<string, Record<string, Knock>> = {};
type KnockClient = ReturnType<typeof createKnockClient>;

const createKnockClient = (config: Config) => {
const serviceToken = config.serviceToken ?? process.env.KNOCK_SERVICE_TOKEN;

if (!serviceToken) {
throw new Error(
"Service token is required. Please set the `serviceToken` property in the config or the `KNOCK_SERVICE_TOKEN` environment variable."
);
}

const client = new KnockMgmt({
serviceToken: config.serviceToken,
serviceToken,
});

return Object.assign(client, {
Expand All @@ -18,8 +26,8 @@ const createKnockClient = (config: Config) => {
environmentSlug ?? config.environment ?? "development";

// If the client already exists for this service token and environment, return it
if (serviceTokensToApiClients?.[config.serviceToken]?.[environment]) {
return serviceTokensToApiClients[config.serviceToken][environment];
if (serviceTokensToApiClients?.[serviceToken]?.[environment]) {
return serviceTokensToApiClients[serviceToken][environment];
}

// Otherwise, fetch a public API key for this service token and environment
Expand All @@ -29,11 +37,11 @@ const createKnockClient = (config: Config) => {
const knock = new Knock(api_key);

// Store the client in the cache
if (!serviceTokensToApiClients[config.serviceToken]) {
serviceTokensToApiClients[config.serviceToken] = {};
if (!serviceTokensToApiClients[serviceToken]) {
serviceTokensToApiClients[serviceToken] = {};
}

serviceTokensToApiClients[config.serviceToken][environment] = knock;
serviceTokensToApiClients[serviceToken][environment] = knock;

return knock;
},
Expand Down
10 changes: 6 additions & 4 deletions src/lib/tools/workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,10 @@ export const workflows = {

export const permissions = {
read: ["listWorkflows", "getWorkflow"],
manage: ["createWorkflow", "createOneOffWorkflowSchedule"].concat(
...Object.keys(workflowStepTools)
),
run: ["triggerWorkflow"],
manage: [
"createWorkflow",
"createOneOffWorkflowSchedule",
"triggerWorkflow",
].concat(...Object.keys(workflowStepTools)),
trigger: [],
};
12 changes: 10 additions & 2 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,16 @@ export async function getToolsByPermissionsInCategories(

// If the user has run permissions for workflows, then we need to get the workflow triggers tools,
// and add them to the list of tools for the workflows category.
if (config.permissions.workflows?.run) {
const workflowTools = await createWorkflowTools(knockClient, config);
if (
config.permissions.workflows &&
config.permissions.workflows.trigger &&
Array.isArray(config.permissions.workflows.trigger)
) {
const workflowTools = await createWorkflowTools(
knockClient,
config,
config.permissions.workflows.trigger
);
toolsByCategory.workflows = [
...toolsByCategory.workflows,
...workflowTools,
Expand Down
11 changes: 4 additions & 7 deletions src/openai/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type KnockToolkit = {
/**
* Creates a Knock toolkit for use with the OpenAI API.
*
* When the `config.permissions.workflows.run` is set, then workflow triggers for
* When the `config.permissions.workflows.trigger` is set, then workflow triggers for
* the specified workflows will be included in the returned tools.
*
* You can also specify a list of workflow keys to include in the returned tools, should you wish to
Expand Down Expand Up @@ -53,8 +53,7 @@ const createKnockToolkit = async (

return Promise.resolve({
/**
* Get all tools as a flat list. When the `config.permissions.workflows.run` is set, then workflow triggers for
* the specified workflows will be included in the returned tools.
* Get all tools as a flat list, including workflow triggers when workflows-as-tools are enabled.
*
* @returns An array of all tools
*/
Expand All @@ -63,8 +62,7 @@ const createKnockToolkit = async (
},

/**
* Get all tools for a specific category. When the `config.permissions.workflows.run` is set, then workflow triggers for
* the specified workflows will be included in the returned tools.
* Get all tools for a specific category, including workflow triggers when workflows-as-tools are enabled.
*
* @param category - The category of tools to get
* @returns An array of tools for the given category
Expand All @@ -76,8 +74,7 @@ const createKnockToolkit = async (
},

/**
* Get a map of all tools by method name. When the `config.permissions.workflows.run` is set, then workflow triggers for
* the specified workflows will be included in the returned tools.
* Get a map of all tools by method name, including workflow triggers when workflows-as-tools are enabled.
*
* @returns A map of all tools by method name
*/
Expand Down
14 changes: 9 additions & 5 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { toolPermissions } from "./lib/tools/index.js";

export interface Config {
/**
* The token to use to authenticate with the service.
* The token to use to authenticate with the service. If not provided, the `serviceToken`
* will be resolved from `KNOCK_SERVICE_TOKEN` environment variable.
*/
serviceToken: string;
serviceToken?: string | undefined;

/**
* When set calls will be made as this user.
Expand Down Expand Up @@ -39,7 +40,10 @@ export interface ToolkitConfig extends Config {
/**
* The permissions to use for the toolkit.
*/
permissions: TransformPermissions<typeof toolPermissions> & {
permissions: Omit<
TransformPermissions<typeof toolPermissions>,
"workflows"
> & {
workflows?: {
/**
* Whether to allow reading workflows.
Expand All @@ -52,11 +56,11 @@ export interface ToolkitConfig extends Config {
manage?: boolean | undefined;

/**
* Optionally specify a list of workflow keys to allow to be run.
* Optionally specify a list of workflow keys to turn into workflow trigger tools
*
* If true, all workflows will be allowed.
*/
run?: string[] | boolean | undefined;
trigger?: string[] | undefined;
};
};
}
Expand Down