diff --git a/.changeset/mighty-boats-listen.md b/.changeset/mighty-boats-listen.md new file mode 100644 index 0000000..bba07dc --- /dev/null +++ b/.changeset/mighty-boats-listen.md @@ -0,0 +1,5 @@ +--- +"@knocklabs/agent-toolkit": patch +--- + +fix: pre-launch fixes diff --git a/README.md b/README.md index ef20226..42cebda 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/examples/ai-sdk/hitl.ts b/examples/ai-sdk/hitl.ts index 5789d95..efc5ed3 100644 --- a/examples/ai-sdk/hitl.ts +++ b/examples/ai-sdk/hitl.ts @@ -13,12 +13,11 @@ 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; @@ -26,14 +25,17 @@ dotenv.config(); }); // 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.", }); diff --git a/examples/ai-sdk/index.ts b/examples/ai-sdk/index.ts index 3b74894..2b87861 100644 --- a/examples/ai-sdk/index.ts +++ b/examples/ai-sdk/index.ts @@ -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); + } })(); diff --git a/examples/ai-sdk/package.json b/examples/ai-sdk/package.json index c5092f4..6b8e218 100644 --- a/examples/ai-sdk/package.json +++ b/examples/ai-sdk/package.json @@ -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", diff --git a/src/ai-sdk/index.ts b/src/ai-sdk/index.ts index bce681a..859111a 100644 --- a/src/ai-sdk/index.ts +++ b/src/ai-sdk/index.ts @@ -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 @@ -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 */ @@ -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 @@ -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 */ diff --git a/src/lib/__tests__/utils.test.ts b/src/lib/__tests__/utils.test.ts index 179ff46..465fadc 100644 --- a/src/lib/__tests__/utils.test.ts +++ b/src/lib/__tests__/utils.test.ts @@ -48,7 +48,8 @@ vi.mock("../tools/index.js", () => ({ }, workflows: { read: ["listWorkflows"], - run: ["triggerWorkflow"], + manage: ["triggerWorkflow"], + trigger: [], }, }, })); @@ -207,10 +208,10 @@ 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( @@ -218,10 +219,8 @@ describe("utils", () => { 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"); }); }); diff --git a/src/lib/knock-client.ts b/src/lib/knock-client.ts index 71b397d..07c00d1 100644 --- a/src/lib/knock-client.ts +++ b/src/lib/knock-client.ts @@ -8,8 +8,16 @@ const serviceTokensToApiClients: Record> = {}; type KnockClient = ReturnType; 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, { @@ -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 @@ -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; }, diff --git a/src/lib/tools/workflows.ts b/src/lib/tools/workflows.ts index 9251747..80d2eef 100644 --- a/src/lib/tools/workflows.ts +++ b/src/lib/tools/workflows.ts @@ -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: [], }; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index fd36753..1397575 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -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, diff --git a/src/openai/index.ts b/src/openai/index.ts index 327ca30..145c8d0 100644 --- a/src/openai/index.ts +++ b/src/openai/index.ts @@ -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 @@ -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 */ @@ -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 @@ -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 */ diff --git a/src/types.ts b/src/types.ts index c4a0908..5454f7d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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. @@ -39,7 +40,10 @@ export interface ToolkitConfig extends Config { /** * The permissions to use for the toolkit. */ - permissions: TransformPermissions & { + permissions: Omit< + TransformPermissions, + "workflows" + > & { workflows?: { /** * Whether to allow reading workflows. @@ -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; }; }; }