diff --git a/auth4genai/how-tos/check-google-calendar-availability.mdx b/auth4genai/how-tos/check-google-calendar-availability.mdx index 81e1b4e7b..0af9d2103 100644 --- a/auth4genai/how-tos/check-google-calendar-availability.mdx +++ b/auth4genai/how-tos/check-google-calendar-availability.mdx @@ -12,48 +12,40 @@ import LlamaIndexJSSample from "/snippets/how-tos/google-calendar/llamaindex.mdx import LlamaIndexSample from "/snippets/how-tos/google-calendar/llamaindex-python.mdx"; import NextJSAuth0Sample from "/snippets/how-tos/google-calendar/nextjs-auth0.mdx"; import GenKitSample from "/snippets/how-tos/google-calendar/genkit.mdx"; -import CreateCustomApiClient from "/snippets/common/create-custom-api-client.mdx"; + +import { GoogleCalendarPrereqs } from "/snippets/how-tos/google-calendar/prereqs.jsx"; +import { CustomApiClient } from "/snippets/common/custom-api-client.jsx"; - - - Before using this example, make sure you: - - - Install Node.js 18+ and `npm`. - - [Set up an OpenAI API key](https://platform.openai.com/docs/quickstart?api-mode=chat). - - Complete the [User authentication quickstart](/get-started/user-authentication) to create a Next.js app integrated with Auth0. - - Set up and configure a Google Cloud project: - - Enable the [Google Calendar API](https://console.cloud.google.com/apis/library/calendar-json.googleapis.com). - - Create OAuth 2.0 credentials (Web Application) with proper redirect URIs. - - Configure a [Social Connection for Google in Auth0](https://marketplace.auth0.com/integrations/google-social-connection) - - Make sure to enable `Token Vault` - - Select `Offline Access` scope - - - + + + + + + @@ -61,23 +53,14 @@ import CreateCustomApiClient from "/snippets/common/create-custom-api-client.mdx - - - Before using this example, make sure you: - - - Install Python 3.11+ and `pip`. - - [Set up an OpenAI API key](https://platform.openai.com/docs/quickstart?api-mode=chat). - - Configure a [Social Connection for Google in Auth0](https://marketplace.auth0.com/integrations/google-social-connection) - - Make sure to enable `Token Vault` - - Select `Offline Access` scope - - + + diff --git a/auth4genai/how-tos/get-salesforce-opportunities.mdx b/auth4genai/how-tos/get-salesforce-opportunities.mdx index 71fcaa934..f5665f827 100644 --- a/auth4genai/how-tos/get-salesforce-opportunities.mdx +++ b/auth4genai/how-tos/get-salesforce-opportunities.mdx @@ -4,28 +4,13 @@ description: "Use OpenAI, NextJS, and the Auth0-AI SDKs to get Salesforce opport mode: "wide" --- +import { SalesforcePrereqs } from "/snippets/how-tos/salesforce/prereqs.jsx"; +import { CustomApiClient } from "/snippets/common/custom-api-client.jsx"; + - - -Before using this example, make sure you: - -- [Set up an OpenAI API key](https://platform.openai.com/docs/quickstart?api-mode=chat). -- Set up and configure a Salesforce instance with an [External Client App](https://help.salesforce.com/s/articleView?id=xcloud.external_client_apps.htm&type=5). -- [Configure a Salesforce OIDC connection](https://auth0.com/docs/authenticate/identity-providers/enterprise-identity-providers/oidc) in Auth0 with the following scopes: - - `openid` - - `api` - - `refresh_token` - - `offline_access` -- Complete the [User authentication quickstart](/get-started/user-authentication) to create a Next.js app integrated with Auth0. -- Set the `SALESFORCE_INSTANCE_URL` in your `.env` file. - -```bash .env wrap lines -SALESFORCE_INSTANCE_URL=https://your-instance.salesforce.com -``` - - + ## 1. Define the Vercel AI tool and backend API route diff --git a/auth4genai/how-tos/list-github-repositories.mdx b/auth4genai/how-tos/list-github-repositories.mdx index 3c36fc0c2..95115899f 100644 --- a/auth4genai/how-tos/list-github-repositories.mdx +++ b/auth4genai/how-tos/list-github-repositories.mdx @@ -13,45 +13,41 @@ import NextJSAuth0Sample from "/snippets/how-tos/github/nextjs-auth0.mdx"; import GenKitSample from "/snippets/how-tos/github/genkit.mdx"; import LlamaIndexJSSample from "/snippets/how-tos/github/llamaindex.mdx"; import AccountLinking from "/snippets/how-tos/account-linking.mdx"; -import CreateCustomApiClient from "/snippets/common/create-custom-api-client.mdx"; + +import { GitHubPrereqs } from "/snippets/how-tos/github/prereqs.jsx"; +import { CustomApiClient } from "/snippets/common/custom-api-client.jsx"; - - Before using this example, make sure you: - - - Install Node.js 18+ and `npm`. - - [Set up an OpenAI API key](https://platform.openai.com/docs/quickstart?api-mode=chat). - - Complete the [User authentication quickstart](/get-started/user-authentication) to create a Next.js app integrated with Auth0. - - Create and configure a [GitHub App](https://docs.github.com/en/apps/creating-github-apps/about-creating-github-apps/about-creating-github-apps). - - Configure a [Social Connection for GitHub in Auth0](https://marketplace.auth0.com/integrations/github-social-connection) - - Make sure to enable `Token Vault` - - - + + + + + + @@ -59,22 +55,14 @@ import CreateCustomApiClient from "/snippets/common/create-custom-api-client.mdx - - - Before using this example, make sure you: - - - Install Python 3.11+ and `pip`. - - [Set up an OpenAI API key](https://platform.openai.com/docs/quickstart?api-mode=chat). - - Configure a [Social Connection for GitHub in Auth0](https://marketplace.auth0.com/integrations/github-social-connection) - - Make sure to enable `Token Vault` - - + + diff --git a/auth4genai/how-tos/list-slack-channels.mdx b/auth4genai/how-tos/list-slack-channels.mdx index 78194ee81..50b748f1d 100644 --- a/auth4genai/how-tos/list-slack-channels.mdx +++ b/auth4genai/how-tos/list-slack-channels.mdx @@ -11,40 +11,35 @@ import LangGraphSample from "/snippets/how-tos/slack/langgraph-python.mdx"; import LlamaIndexSample from "/snippets/how-tos/slack/llamaindex-python.mdx"; import GenKitSample from "/snippets/how-tos/slack/genkit.mdx"; import LlamaIndexJSSample from "/snippets/how-tos/slack/llamaindex.mdx"; -import CreateCustomApiClient from "/snippets/common/create-custom-api-client.mdx"; + +import { SlackPrereqs } from "/snippets/how-tos/slack/prereqs.jsx"; +import { CustomApiClient } from "/snippets/common/custom-api-client.jsx"; - - - Before using this example, make sure you: - - - Install Node.js 18+ and `npm`. - - [Set up an OpenAI API key](https://platform.openai.com/docs/quickstart?api-mode=chat). - - Complete the [User authentication quickstart](/get-started/user-authentication) to create a Next.js app integrated with Auth0. - - Configure a [Social Connection for Slack in Auth0](https://marketplace.auth0.com/integrations/sign-in-with-slack) - - Make sure to enable `Token Vault` - - - + + + + + @@ -52,22 +47,14 @@ import CreateCustomApiClient from "/snippets/common/create-custom-api-client.mdx - - - Before using this example, make sure you: - - - Install Python 3.11+ and `pip`. - - [Set up an OpenAI API key](https://platform.openai.com/docs/quickstart?api-mode=chat). - - Configure a [Social Connection for Slack in Auth0](https://marketplace.auth0.com/integrations/sign-in-with-slack) - - Make sure to enable `Token Vault` - - + + diff --git a/auth4genai/snippets/common/configure-cloudflare-kv-store.mdx b/auth4genai/snippets/common/configure-cloudflare-kv-store.mdx new file mode 100644 index 000000000..adee9e143 --- /dev/null +++ b/auth4genai/snippets/common/configure-cloudflare-kv-store.mdx @@ -0,0 +1,10 @@ +For persisting Auth0 session data and other key-value pairs, you need to configure a persistent store with your Cloudflare agent worker. When constructing the `CloudflareKVStore` instance with your Cloudflare agent worker, you can use Workers KV and a [KV namespace](https://developers.cloudflare.com/kv/get-started/) as the persistent store. This enables you to store Auth0 session data and other key-value pairs with easy access from your Cloudflare agent workers. + +```ts +import { CloudflareKVStore } from '@auth0/ai-cloudflare'; +... + +return new CloudflareKVStore({ kv: this.env.YOUR_KV_NAMESPACE }); +``` + +**Note:** the `kv` prop accepts any store which implements the [`KVNamespace` interface](https://github.com/auth0/auth0-ai-js/blob/%40auth0/ai-cloudflare-v2.0.0/packages/ai-cloudflare/src/CloudflareKVStore.ts#L3-L14), so any persistent store which implements this interface will work. diff --git a/auth4genai/snippets/common/create-custom-api-client.mdx b/auth4genai/snippets/common/create-custom-api-client.mdx deleted file mode 100644 index 24cb33858..000000000 --- a/auth4genai/snippets/common/create-custom-api-client.mdx +++ /dev/null @@ -1,27 +0,0 @@ -- Create a Custom API Client: -
- The Custom API Client allows your API server to perform token - exchanges using access tokens - instead of refresh tokens. - This client enables Token Vault to exchange an access token for an - external API access token (e.g., Google Calendar API). -
-
    -
  • - Navigate to Applications > APIs -
  • -
  • - Click the{" "} - Create API button to create a new Custom API. -
  • -
  • - Go to the Custom API you created and click the Add Application button in the right top corner. -
  • -
  • - After that click the Configure Application button in the right top corner. -
  • -
  • - Note down the client id and client secret{" "} - for your environment variables. -
  • -
diff --git a/auth4genai/snippets/common/custom-api-client.jsx b/auth4genai/snippets/common/custom-api-client.jsx new file mode 100644 index 000000000..54f5c1049 --- /dev/null +++ b/auth4genai/snippets/common/custom-api-client.jsx @@ -0,0 +1,43 @@ +export const CustomApiClient = ({ + apiName = "the external API" +}) => { + return ( + + The Custom API Client allows your API server to perform token exchanges + using{" "} + + access tokens + {" "} + instead of{" "} + + refresh tokens + + . This client enables Token Vault to exchange an access token for an + external API access token (e.g., {apiName}). +
+
    +
  • + Navigate to Applications > APIs +
  • +
  • + Click the Create API button to create a new Custom + API. +
  • +
  • + Go to the Custom API you created and click the{" "} + Add Application button in the right top corner. +
  • +
  • + Once you've added the API as an application, click the Configure Application button + in the right top corner. +
  • +
  • + Note down the client id and client secret{" "} + for your environment variables. +
  • +
+
+ ); +}; + + diff --git a/auth4genai/snippets/get-started/cloudflare-agents-js/call-your-api.mdx b/auth4genai/snippets/get-started/cloudflare-agents-js/call-your-api.mdx index 988d26329..abfd06001 100644 --- a/auth4genai/snippets/get-started/cloudflare-agents-js/call-your-api.mdx +++ b/auth4genai/snippets/get-started/cloudflare-agents-js/call-your-api.mdx @@ -1,5 +1,6 @@ import { Prerequisites } from "/snippets/get-started/prerequisites/call-your-api.jsx"; import { AccountAndAppSteps } from "/snippets/get-started/prerequisites/account-app-steps.jsx"; +import ConfigureCloudflareKvStore from "/snippets/common/configure-cloudflare-kv-store.mdx"; @@ -42,6 +43,27 @@ npm install hono \ In the root directory of your project, copy the `.dev.vars.example` into `.dev.vars` file and configure the Auth0 and OpenAI variables. +```bash .dev.vars wrap lines +# ... +# You can use any provider of your choice supported by Vercel AI +OPENAI_API_KEY="OPENAI API KEY" + +#auth0 +AUTH0_DOMAIN="YOUR-ACCOUNT.us.auth0.com" +AUTH0_CLIENT_ID="YOUR CLIENT ID" +AUTH0_CLIENT_SECRET="YOUR CLIENT SECRET" +AUTH0_SESSION_ENCRYPTION_KEY="RANDOM 32 CHARS" +AUTH0_AUDIENCE="YOUR AUDIENCE" + +BASE_URL="http://localhost:3000" +``` + +If you use another provider for your LLM, adjust the variable name in `.dev.vars` accordingly. + +#### Configure a persistent store + + + ### Define a tool to call your API In this step, you'll create a Vercel AI tool to make the first-party API call to the Auth0 API. You will do the same for third-party APIs. @@ -53,41 +75,159 @@ Since the Agent defined in the class Chat in `src/agent/chat.ts` uses the **Auth The tool we are defining here uses the same access token to call Auth0's [`/userinfo`](https://auth0.com/docs/api/authentication/user-profile/get-user-info) endpoint. ```tsx src/agent/tools.ts wrap lines -const getUserInfoTool = tool({ - description: "Get information about the current logged in user.", - parameters: z.object({}), - execute: async () => { - const { agent } = getCurrentAgent(); - const tokenSet = agent?.getCredentials(); - if (!tokenSet) { - return "There is no user logged in."; +/** + * Tool definitions for the AI chat agent + * Tools can either require human confirmation or execute automatically + */ +import { tool } from "ai"; +import { z } from "zod"; + +import { getCurrentAgent } from "agents"; +import { unstable_scheduleSchema } from "agents/schedule"; +import { format, toZonedTime } from "date-fns-tz"; +import { buyStock } from "./auth0-ai-sample-tools/buy-stock"; +import { checkUsersCalendar } from "./auth0-ai-sample-tools/check-user-calendar"; + +/** + * Weather information tool that requires human confirmation + * When invoked, this will present a confirmation dialog to the user + * The actual implementation is in the executions object below + */ +const getWeatherInformation = tool({ + description: "show the weather in a given city to the user", + inputSchema: z.object({ city: z.string() }), +}); + +/** + * Local time tool that executes automatically + * Since it includes an execute function, it will run without user confirmation + * This is suitable for low-risk operations that don't need oversight + */ +const getLocalTime = tool({ + description: "get the local time for a specified location", + inputSchema: z.object({ + timeZone: z.string().describe("IANA time zone name"), + }), + execute: async ({ timeZone: location }) => { + const now = new Date(); + const zonedDate = toZonedTime(now, location); + const output = format(zonedDate, "yyyy-MM-dd HH:mm:ssXXX", { + timeZone: location, + }); + return output; + }, +}); + +const scheduleTask = tool({ + description: "A tool to schedule a task to be executed at a later time", + inputSchema: unstable_scheduleSchema, + execute: async ({ when, description }) => { + const { agent } = getCurrentAgent(); + + function throwError(msg: string): string { + throw new Error(msg); + } + if (when.type === "no-schedule") { + return "Not a valid schedule input"; + } + const input = + when.type === "scheduled" + ? when.date // scheduled + : when.type === "delayed" + ? when.delayInSeconds // delayed + : when.type === "cron" + ? when.cron // cron + : throwError("not a valid schedule input"); + try { + agent!.schedule(input!, "executeTask" as keyof typeof agent, description); + } catch (error) { + console.error("error scheduling task", error); + return `Error scheduling task: ${error}`; } + return `Task scheduled for type "${when.type}" : ${input}`; + }, +}); - const response = await fetch( - `https://${process.env.AUTH0_DOMAIN}/userinfo`, - { - headers: { - Authorization: `Bearer ${tokenSet.access_token}`, - }, - } - ); +/** + * Tool to list all scheduled tasks + * This executes automatically without requiring human confirmation + */ +const getScheduledTasks = tool({ + description: "List all tasks that have been scheduled", + inputSchema: z.object({}), + execute: async () => { + const { agent } = getCurrentAgent(); - if (response.ok) { - return { result: await response.json() }; + try { + const tasks = agent!.getSchedules(); + if (!tasks || tasks.length === 0) { + return "No scheduled tasks found."; + } + return tasks; + } catch (error) { + console.error("Error listing scheduled tasks", error); + return `Error listing scheduled tasks: ${error}`; } + }, +}); - return "I couldn't verify your identity"; +/** + * Tool to cancel a scheduled task by its ID + * This executes automatically without requiring human confirmation + */ +const cancelScheduledTask = tool({ + description: "Cancel a scheduled task using its ID", + inputSchema: z.object({ + taskId: z.string().describe("The ID of the task to cancel"), + }), + execute: async ({ taskId }) => { + const { agent } = getCurrentAgent(); + try { + await agent!.cancelSchedule(taskId); + return `Task ${taskId} has been successfully canceled.`; + } catch (error) { + console.error("Error canceling scheduled task", error); + return `Error canceling task ${taskId}: ${error}`; + } }, }); + +/** + * Export all available tools + * These will be provided to the AI model to describe available capabilities + */ +export const tools = { + getWeatherInformation, + getLocalTime, + scheduleTask, + getScheduledTasks, + cancelScheduledTask, + checkUsersCalendar, + buyStock, +}; + +/** + * Implementation of confirmation-required tools + * This object contains the actual logic for tools that need human approval + * Each function here corresponds to a tool above that doesn't have an execute function + */ +export const executions = { + getWeatherInformation: async ({ city }: { city: string }) => { + console.log(`Getting weather information for ${city}`); + return `The weather in ${city} is sunny`; + }, +}; ``` -Then in the `tools` export of the `src/agent/chat.ts` file, add the `getUserInfoTool` to the tools array: +Then in the `tools` export of the `src/agent/chat.ts` file, add the `tools` to the `allTools` array: ```tsx src/agent/chat.ts wrap lines -export const tools = { - // Your other tools... - getUserInfoTool, -}; +async onChatMessage() { + const allTools = { + ...tools, + ...(this.mcp?.getAITools?.() ?? {}), + }; + ... // The rest of the code ``` ### Test your application @@ -104,4 +244,4 @@ AI: You are Juan Martinez. Here are your details: - ......... That's it! You've successfully integrated first-party tool-calling into your project. -Explore [the start kit on GitHub](https://github.com/auth0-lab/cloudflare-agents-starter). +Explore [the starter kit on GitHub](https://github.com/auth0-lab/cloudflare-agents-starter). diff --git a/auth4genai/snippets/how-tos/github/cloudflare-agents.mdx b/auth4genai/snippets/how-tos/github/cloudflare-agents.mdx index c5b6f3dcc..6fe3c8d86 100644 --- a/auth4genai/snippets/how-tos/github/cloudflare-agents.mdx +++ b/auth4genai/snippets/how-tos/github/cloudflare-agents.mdx @@ -1,3 +1,5 @@ +import ConfigureCloudflareKvStore from "/snippets/common/configure-cloudflare-kv-store.mdx"; + ### 1. Configure Auth0 AI @@ -19,30 +21,35 @@ npm install @auth0/ai-vercel @auth0/ai-cloudflare @auth0/ai Then, you need to initialize Auth0 AI and set up the connection to request access tokens with the required GitHub scopes. -```typescript ./src/lib/auth0-ai.ts wrap lines +```typescript ./src/agent/auth0-ai.ts wrap lines import { Auth0AI, setGlobalAIContext } from "@auth0/ai-vercel"; import { getCurrentAgent } from "agents"; -import type { Chat } from "./chat"; + +setGlobalAIContext(() => ({ threadID: getAgent().name })); + +const auth0AI = new Auth0AI({ + store: () => { + return (getAgent() as any).auth0AIStore; + }, +}); const getAgent = () => { - const { agent } = getCurrentAgent(); + const { agent } = getCurrentAgent(); if (!agent) { throw new Error("No agent found"); } return agent; }; -setGlobalAIContext(() => ({ threadID: getAgent().name })); - -const auth0AI = new Auth0AI(); +const refreshToken = async () => { + const credentials = getAgent().getCredentials(); + return credentials?.refresh_token; +}; export const withGitHub = auth0AI.withTokenVault({ + refreshToken, connection: "github", scopes: ["repo"], - refreshToken: async () => { - const credentials = getAgent().getCredentials(); - return credentials?.refresh_token; - }, }); ``` @@ -50,19 +57,20 @@ export const withGitHub = auth0AI.withTokenVault({ Wrap your tool using the Auth0 AI SDK to obtain an access token for the GitHub API. -```typescript ./src/agent/tools/listRepositories.ts wrap lines highlight={2-4,9,15,19-21,31-33} +```typescript ./src/agent/tools/listRepositories.ts wrap lines highlight={7-8,10,16,20-22,29-31} +import { tool } from "ai"; +import { z } from "zod/v3"; + import { Octokit, RequestError } from "octokit"; import { getAccessTokenFromTokenVault } from "@auth0/ai-vercel"; import { TokenVaultError } from "@auth0/ai/interrupts"; -import { withGitHub } from "@/lib/auth0-ai"; -import { tool } from "ai"; -import { z } from "zod"; - +import { Octokit, RequestError } from "octokit"; +import { withGitHub } from "@/agent/auth0-ai"; export const listRepositories = withGitHub( tool({ description: "List respositories for the current user on GitHub", - parameters: z.object({}), + inputSchema: z.object({}), execute: async () => { // Get the access token from Auth0 AI const accessToken = getAccessTokenFromTokenVault(); @@ -72,17 +80,14 @@ export const listRepositories = withGitHub( const octokit = new Octokit({ auth: accessToken, }); - const { data } = await octokit.rest.repos.listForAuthenticatedUser(); return data.map((repo) => repo.name); } catch (error) { - console.log("Error", error); - if (error instanceof RequestError) { if (error.status === 401) { throw new TokenVaultError( - `Authorization required to access the Token Vault connection` + `Authorization required to access the Token Vault` ); } } @@ -92,6 +97,7 @@ export const listRepositories = withGitHub( }, }) ); + ``` ### 3. Handle authentication redirects @@ -104,80 +110,130 @@ Interrupts are a way for the system to pause execution and prompt the user to ta On the Chat agent class, you need to set up the tool invocation and handle the interruption messaging via the `errorSerializer`. -```typescript ./src/agent/chat.ts wrap lines highlight={1-2,52-54,63-66} -import { setAIContext } from "@auth0/ai-vercel"; -import { errorSerializer, withInterruptions } from "@auth0/ai-vercel/interrupts"; -// Other dependencies -import { AuthAgent, OwnedAgent } from "@auth0/auth0-cloudflare-agents-api"; +```typescript ./src/agent/chat.ts wrap lines highlight={4,6,46,70-84,95} import { openai } from "@ai-sdk/openai"; +import { CloudflareKVStore } from "@auth0/ai-cloudflare"; +import { + errorSerializer, + invokeTools, + withInterruptions, +} from "@auth0/ai-vercel/interrupts"; import { AIChatAgent } from "agents/ai-chat-agent"; +import { + convertToModelMessages, + createUIMessageStream, + createUIMessageStreamResponse, + generateId, + stepCountIs, + streamText, + type UIMessage, +} from "ai"; +import { extend } from "flumix"; +import { executions, tools } from "./tools"; +import { processToolCalls } from "./utils"; + +import { AsyncUserConfirmationResumer } from "@auth0/ai-cloudflare"; +import { AuthAgent, OwnedAgent } from "@auth0/auth0-cloudflare-agents-api"; -const SuperAgent = OwnedAgent(AuthAgent(AIChatAgent)); +const model = openai("gpt-4o-2024-11-20"); + +const SuperAgent = extend(AIChatAgent) + .with(AuthAgent) + .with(OwnedAgent) + .with(AsyncUserConfirmationResumer) + .build(); export class Chat extends SuperAgent { - async onChatMessage( - onFinish: StreamTextOnFinishCallback, - options?: { abortSignal?: AbortSignal } - ) { - // Collect all tools, including MCP tools + messages: UIMessage[] = []; + + async onChatMessage() { const allTools = { ...tools, - ...this.mcp.unstable_getAITools(), + ...(this.mcp?.getAITools?.() ?? {}), }; - const claims = this.getClaims(); - // Create a streaming response that handles both text and tool outputs - const dataStreamResponse = createDataStreamResponse({ - execute: withInterruptions(async (dataStream) => { - // Process any pending tool calls from previous messages - // This handles human-in-the-loop confirmations for tools - const processedMessages = await processToolCalls({ - messages: this.messages, - dataStream, - tools: allTools, - executions, - }); - - // Stream the AI response using GPT-4 - const result = streamText({ - model, - system: `You are a helpful assistant that can do various tasks... -${unstable_getSchedulePrompt({ date: new Date() })} + const claims = this.getClaims?.(); + + const stream = createUIMessageStream({ + originalMessages: this.messages, + execute: withInterruptions( + async ({ writer }) => { + await invokeTools({ + messages: convertToModelMessages(this.messages), + tools: allTools, + }); + + const processed = await processToolCalls({ + messages: this.messages, + dataStream: writer, + tools: allTools, + executions, + }); + + const result = streamText({ + model, + stopWhen: stepCountIs(10), + messages: convertToModelMessages(processed), + system: `You are a helpful assistant that can do various tasks... If the user asks to schedule a task, use the schedule tool to schedule the task. -The name of the user is ${claims?.name ?? "unknown"}. -`, - messages: processedMessages, - tools: allTools, - onFinish: async (args) => { - onFinish( - args as Parameters>[0] - ); - }, - onError: (error) => { - if (!Auth0Interrupt.isInterrupt(error)) { - return; - } - console.error("Error while streaming:", error); - }, - maxSteps: 10, - }); - - // Merge the AI response stream with tool execution outputs - result.mergeIntoDataStream(dataStream); - }), - onError: errorSerializer((err) => { - console.log(err); - return "Oops, an error occured!"; - }), +The name of the user is ${claims?.name ?? "unknown"}.`, + tools: allTools, + onStepFinish: (output) => { + if (output.finishReason === "tool-calls") { + const last = output.content[output.content.length - 1]; + if (last?.type === "tool-error") { + const { toolName, toolCallId, error, input } = last; + const serializableError = { + cause: error, + toolCallId, + toolName, + toolArgs: input, + }; + throw serializableError; + } + } + }, + }); + + writer.merge( + result.toUIMessageStream({ + sendReasoning: true, + }) + ); + }, + { messages: this.messages, tools: allTools } + ), + onError: errorSerializer(), }); - return dataStreamResponse; + return createUIMessageStreamResponse({ stream }); + } + + async executeTask(description: string) { + await this.saveMessages([ + ...this.messages, + { + id: generateId(), + role: "user", + parts: [ + { type: "text", text: `Running scheduled task: ${description}` }, + ], + }, + ]); + } + + get auth0AIStore() { + return new CloudflareKVStore({ kv: this.env.Session }); } } ``` +**Note about CloudflareKVStore:** + + + #### Client Side In this example, we utilize the `TokenVaultConsentPopup` component to show a pop-up that allows the user to authenticate with GitHub and grant access with the requested scopes. You'll first need to install the `@auth0/ai-components` package: @@ -188,7 +244,7 @@ npx @auth0/ai-components add TokenVault Then, you can integrate the authentication popup in your chat component, using the interruptions helper from the SDK: -```tsx ./src/components/chat.tsx wrap lines highlight={4-6,17-18,33-42} +```tsx ./src/client/app.tsx wrap lines highlight={4-6,14-17,24,48-71} "use client"; import { useChat } from "@ai-sdk/react"; @@ -197,45 +253,80 @@ import { TokenVaultInterrupt } from "@auth0/ai/interrupts"; import { TokenVaultConsentPopup } from "@/components/auth0-ai/TokenVault/popup"; export default function Chat() { + const agent = useAgent({ + agent: "chat", + name: threadID ?? undefined, + }); + + const chat = useAgentChatInterruptions({ + agent, + id: threadID, + }); const { messages: agentMessages, - input: agentInput, - handleInputChange: handleAgentInputChange, - handleSubmit: handleAgentSubmit, + sendMessage: handleAgentSubmit, addToolResult, clearHistory, toolInterrupt, - } = useAgentChatInterruptions({ - agent, - maxSteps: 5, - id: threadID, - }); + } = chat; return ( -
- {messages.map((message) => ( -
- {message.role === "user" ? "User: " : "AI: "} - {message.content} -
- ))} - - {TokenVaultInterrupt.isInterrupt(toolInterrupt) && ( - - )} + + {agentMessages.map((m: UIMessage, index) => { + const isUser = m.role === "user"; + + return ( +
+ {showDebug && ( +
+                {JSON.stringify(m, null, 2)}
+              
+ )} +
+
+
+
+ {m.parts?.map((part: any, i) => { + if ( + part?.type?.startsWith("tool-") && + toolInterrupt && + TokenVaultInterrupt.isInterrupt(toolInterrupt) + ) { + return ( + + ); + } +
+
+
+
+
+ ); + })}
- setInput(e.target.value)} /> + setInput(e.target.value)} + />
-
+ ); } ``` diff --git a/auth4genai/snippets/how-tos/github/prereqs.jsx b/auth4genai/snippets/how-tos/github/prereqs.jsx new file mode 100644 index 000000000..c8b9aeb5f --- /dev/null +++ b/auth4genai/snippets/how-tos/github/prereqs.jsx @@ -0,0 +1,111 @@ +export const GitHubPrereqs = ({ + lang, + createCustomApiClientStep = false, +}) => { + const languageSteps = []; + + if (lang === "js") { + languageSteps.push( + + Install Node.js 20+ and npm + + } + />, + ); + } else if (lang === "python") { + languageSteps.push( + + Install Python 3.11+ and pip + + } + /> + ); + } + + const commonSteps = [ + + Complete the{" "} + + User authentication quickstart + {" "} + to create an application integrated with Auth0. + + } + />, + + + Set up an OpenAI API key + + . + + } + />, + + Create and configure a{" "} + + GitHub App + + . + + } + />, + + Configure a{" "} + + Social Connection for GitHub in Auth0 + + + } + > +
    +
  • + Under the Purpose section, make sure to enable the{" "} + Use for Connected Accounts with Token Vault toggle. +
  • +
  • + Under the Permissions section, enable the Offline Access{" "} + scope. +
  • +
+
, + ]; + + if (createCustomApiClientStep) { + const step = CustomApiClient({ apiName: "Github API" }); + commonSteps.push(step); + } + + return ( + <> + + Prerequisites + + Before getting started, make sure you have completed the following steps: + {[...languageSteps, ...commonSteps]} + + ); +}; diff --git a/auth4genai/snippets/how-tos/google-calendar/cloudflare-agents.mdx b/auth4genai/snippets/how-tos/google-calendar/cloudflare-agents.mdx index de6e32c97..271ce972a 100644 --- a/auth4genai/snippets/how-tos/google-calendar/cloudflare-agents.mdx +++ b/auth4genai/snippets/how-tos/google-calendar/cloudflare-agents.mdx @@ -1,14 +1,20 @@ +import ConfigureCloudflareKvStore from "/snippets/common/configure-cloudflare-kv-store.mdx"; + ### 1. Configure Auth0 AI -If you started from the [Auth0 Cloudflare Agents starter kit](https://github.com/auth0-lab/cloudflare-agents-starter), you can skip this step as the Auth0 AI SDK is already configured. + + If you started from the [Auth0 Cloudflare Agents starter + kit](https://github.com/auth0-lab/cloudflare-agents-starter), you can skip + this step as the Auth0 AI SDK is already configured. + First, you must configure your Cloudflare Agent to use Auth0 and both in the Worker and in the Chat Agent itself. We recommend the following two sdks: + - [Auth0 Hono Web SDK](https://github.com/auth0-lab/auth0-hono): for the Worker. - [Auth0 Cloudflare Agents API SDK](https://github.com/auth0-lab/auth0-cloudflare-agents-api) for the Chat Agent. You can also check our [Starter Kit](https://github.com/auth0-lab/cloudflare-agents-starter) to understand how to configure this. - Then, you need to install the Auth0 AI SDK for Cloudflare Agents: ```bash wrap lines @@ -17,7 +23,7 @@ npm install @auth0/ai-vercel @auth0/ai-cloudflare @auth0/ai Then, you need to initialize Auth0 AI and set up the connection to request access tokens with the required Google Calendar scopes. -```typescript ./src/lib/auth0-ai.ts wrap lines +```typescript ./src/agent/auth0-ai.ts wrap lines import { Auth0AI, setGlobalAIContext } from "@auth0/ai-vercel"; import { getCurrentAgent } from "agents"; import type { Chat } from "./chat"; @@ -32,15 +38,21 @@ const getAgent = () => { setGlobalAIContext(() => ({ threadID: getAgent().name })); -const auth0AI = new Auth0AI(); +const auth0AI = new Auth0AI({ + store: () => { + return getAgent().auth0AIStore; + }, +}); + +const refreshToken = async () => { + const credentials = getAgent().getCredentials(); + return credentials?.refresh_token; +}; export const withGoogleCalendar = auth0AI.withTokenVault({ + refreshToken, connection: "google-oauth2", scopes: ["https://www.googleapis.com/auth/calendar.freebusy"], - refreshToken: async () => { - const credentials = getAgent().getCredentials(); - return credentials?.refresh_token; - }, }); ``` @@ -48,60 +60,57 @@ export const withGoogleCalendar = auth0AI.withTokenVault({ Wrap your tool using the Auth0 AI SDK to obtain an access token for the Google Calendar API. -```typescript ./src/agent/tools/checkUsersCalendar.ts wrap lines highlight={4-6,10,19,26-28.46-48} -import { addHours, formatISO } from "date-fns"; -import { GaxiosError } from "gaxios"; -import { google } from "googleapis"; +```typescript ./src/agent/auth0-ai-sample-tools/check-user-calendar.ts wrap lines highlight={1-2,6,8,17,37-39} import { getAccessTokenFromTokenVault } from "@auth0/ai-vercel"; import { TokenVaultError } from "@auth0/ai/interrupts"; -import { withGoogleCalendar } from "@/lib/auth0-ai"; import { tool } from "ai"; +import { addHours } from "date-fns"; import { z } from "zod"; +import { withGoogleCalendar } from "../auth0-ai"; export const checkUsersCalendar = withGoogleCalendar( tool({ description: "Check user availability on a given date time on their calendar", - parameters: z.object({ + inputSchema: z.object({ date: z.coerce.date(), }), execute: async ({ date }) => { // Get the access token from Auth0 AI const accessToken = getAccessTokenFromTokenVault(); - - // Google SDK - try { - const calendar = google.calendar("v3"); - const auth = new google.auth.OAuth2(); - - auth.setCredentials({ - access_token: accessToken, - }); - - const response = await calendar.freebusy.query({ - auth, - requestBody: { - timeMin: formatISO(date), - timeMax: addHours(date, 1).toISOString(), - timeZone: "UTC", - items: [{ id: "primary" }], - }, - }); - - return { - available: response.data?.calendars?.primary?.busy?.length === 0, - }; - } catch (error) { - if (error instanceof GaxiosError) { - if (error.status === 401) { - throw new TokenVaultError( - `Authorization required to access the Token Vault connection` - ); - } + const url = "https://www.googleapis.com/calendar/v3/freeBusy"; + const body = JSON.stringify({ + timeMin: date, + timeMax: addHours(date, 1), + timeZone: "UTC", + items: [{ id: "primary" }], + }); + + const response = await fetch(url, { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body, + }); + + if (!response.ok) { + if (response.status === 401) { + throw new TokenVaultError( + "Authorization required to access the Federated Connection" + ); } - - throw error; + throw new Error( + `Invalid response from Google Calendar API: ${ + response.status + } - ${await response.text()}` + ); } + + const busyResp = await response.json(); + + return { available: busyResp.calendars.primary.busy.length === 0 }; }, }) ); @@ -111,86 +120,140 @@ export const checkUsersCalendar = withGoogleCalendar( Interrupts are a way for the system to pause execution and prompt the user to take an action—such as authenticating or granting API access—before resuming the interaction. This ensures that any required access is granted dynamically and securely during the chat experience. In this context, Auth0-AI SDK manages authentication redirects in the Vercel AI SDK via these interrupts. -If you started from the [Auth0 Cloudflare Agents starter kit](https://github.com/auth0-lab/cloudflare-agents-starter), you can skip this section as the Auth0 AI SDK is already configured to handle interrupts. + + If you started from the [Auth0 Cloudflare Agents starter + kit](https://github.com/auth0-lab/cloudflare-agents-starter), you can skip + this section as the Auth0 AI SDK is already configured to handle interrupts. + #### Server Side On the Chat agent class, you need to set up the tool invocation and handle the interruption messaging via the `errorSerializer`. -```typescript ./src/agent/chat.ts wrap lines highlight={1-2,52-54,63-66} -import { setAIContext } from "@auth0/ai-vercel"; -import { errorSerializer, withInterruptions } from "@auth0/ai-vercel/interrupts"; -// Other dependencies -import { AuthAgent, OwnedAgent } from "@auth0/auth0-cloudflare-agents-api"; +```typescript ./src/agent/chat.ts wrap lines highlight={5-6,46,70-84,95} import { openai } from "@ai-sdk/openai"; +import { CloudflareKVStore } from "@auth0/ai-cloudflare"; +import { + errorSerializer, + invokeTools, + withInterruptions, +} from "@auth0/ai-vercel/interrupts"; import { AIChatAgent } from "agents/ai-chat-agent"; +import { + convertToModelMessages, + createUIMessageStream, + createUIMessageStreamResponse, + generateId, + stepCountIs, + streamText, + type UIMessage, +} from "ai"; +import { extend } from "flumix"; +import { executions, tools } from "./tools"; +import { processToolCalls } from "./utils"; + +import { AsyncUserConfirmationResumer } from "@auth0/ai-cloudflare"; +import { AuthAgent, OwnedAgent } from "@auth0/auth0-cloudflare-agents-api"; + +const model = openai("gpt-4o-2024-11-20"); -const SuperAgent = OwnedAgent(AuthAgent(AIChatAgent)); +const SuperAgent = extend(AIChatAgent) + .with(AuthAgent) + .with(OwnedAgent) + .with(AsyncUserConfirmationResumer) + .build(); export class Chat extends SuperAgent { - async onChatMessage( - onFinish: StreamTextOnFinishCallback, - options?: { abortSignal?: AbortSignal } - ) { - // Collect all tools, including MCP tools + messages: UIMessage[] = []; + + async onChatMessage() { const allTools = { ...tools, - ...this.mcp.unstable_getAITools(), + ...(this.mcp?.getAITools?.() ?? {}), }; - const claims = this.getClaims(); - // Create a streaming response that handles both text and tool outputs - const dataStreamResponse = createDataStreamResponse({ - execute: withInterruptions(async (dataStream) => { - // Process any pending tool calls from previous messages - // This handles human-in-the-loop confirmations for tools - const processedMessages = await processToolCalls({ - messages: this.messages, - dataStream, - tools: allTools, - executions, - }); - - // Stream the AI response using GPT-4 - const result = streamText({ - model, - system: `You are a helpful assistant that can do various tasks... - -${unstable_getSchedulePrompt({ date: new Date() })} + + const claims = this.getClaims?.(); + + const stream = createUIMessageStream({ + originalMessages: this.messages, + execute: withInterruptions( + async ({ writer }) => { + await invokeTools({ + messages: convertToModelMessages(this.messages), + tools: allTools, + }); + + const processed = await processToolCalls({ + messages: this.messages, + dataStream: writer, + tools: allTools, + executions, + }); + + const result = streamText({ + model, + stopWhen: stepCountIs(10), + messages: convertToModelMessages(processed), + system: `You are a helpful assistant that can do various tasks... If the user asks to schedule a task, use the schedule tool to schedule the task. -The name of the user is ${claims?.name ?? "unknown"}. -`, - messages: processedMessages, - tools: allTools, - onFinish: async (args) => { - onFinish( - args as Parameters>[0] - ); - }, - onError: (error) => { - if (!Auth0Interrupt.isInterrupt(error)) { - return; - } - console.error("Error while streaming:", error); - }, - maxSteps: 10, - }); - - // Merge the AI response stream with tool execution outputs - result.mergeIntoDataStream(dataStream); - }), - onError: errorSerializer((err) => { - console.log(err); - return "Oops, an error occured!"; - }), +The name of the user is ${claims?.name ?? "unknown"}.`, + tools: allTools, + onStepFinish: (output) => { + if (output.finishReason === "tool-calls") { + const last = output.content[output.content.length - 1]; + if (last?.type === "tool-error") { + const { toolName, toolCallId, error, input } = last; + const serializableError = { + cause: error, + toolCallId, + toolName, + toolArgs: input, + }; + throw serializableError; + } + } + }, + }); + + writer.merge( + result.toUIMessageStream({ + sendReasoning: true, + }) + ); + }, + { messages: this.messages, tools: allTools } + ), + onError: errorSerializer(), }); - return dataStreamResponse; + return createUIMessageStreamResponse({ stream }); + } + + async executeTask(description: string) { + await this.saveMessages([ + ...this.messages, + { + id: generateId(), + role: "user", + parts: [ + { type: "text", text: `Running scheduled task: ${description}` }, + ], + }, + ]); + } + + get auth0AIStore() { + return new CloudflareKVStore({ kv: this.env.Session }); } } ``` +**Note about CloudflareKVStore:** + + + #### Client Side In this example, we utilize the `TokenVaultConsentPopup` component to show a pop-up that allows the user to authenticate with Google Calendar and grant access with the requested scopes. You'll first need to install the `@auth0/ai-components` package: @@ -201,7 +264,7 @@ npx @auth0/ai-components add TokenVault Then, you can integrate the authentication popup in your chat component, using the interruptions helper from the SDK: -```tsx ./src/components/chat.tsx wrap lines highlight={4-6,17-18,33-42} +```tsx ./src/client/app.tsx wrap lines highlight={4-6,14-17,24,48-71} "use client"; import { useChat } from "@ai-sdk/react"; @@ -210,45 +273,85 @@ import { TokenVaultInterrupt } from "@auth0/ai/interrupts"; import { TokenVaultConsentPopup } from "@/components/auth0-ai/TokenVault/popup"; export default function Chat() { + const agent = useAgent({ + agent: "chat", + name: threadID ?? undefined, + }); + + const chat = useAgentChatInterruptions({ + agent, + id: threadID, + }); const { messages: agentMessages, - input: agentInput, - handleInputChange: handleAgentInputChange, - handleSubmit: handleAgentSubmit, + sendMessage: handleAgentSubmit, addToolResult, clearHistory, toolInterrupt, - } = useAgentChatInterruptions({ - agent, - maxSteps: 5, - id: threadID, - }); + } = chat; return ( -
- {messages.map((message) => ( -
- {message.role === "user" ? "User: " : "AI: "} - {message.content} -
- ))} - - {TokenVaultInterrupt.isInterrupt(toolInterrupt) && ( - - )} + + {agentMessages.map((m: UIMessage, index) => { + const isUser = m.role === "user"; + + return ( +
+ {showDebug && ( +
+                {JSON.stringify(m, null, 2)}
+              
+ )} +
+
+
+
+ {m.parts?.map((part: any, i) => { + if ( + part?.type?.startsWith("tool-") && + toolInterrupt && + TokenVaultInterrupt.isInterrupt(toolInterrupt) + ) { + return ( + + +
+ ), + title: "Google Calendar Access", + description: + "We need access to your google Calendar in order to call this tool...", + action: { label: "Grant" }, + }} + /> + ); + } +
+
+
+
+
+ ); + })}
- setInput(e.target.value)} /> + setInput(e.target.value)} + />
- + ); } ``` diff --git a/auth4genai/snippets/how-tos/google-calendar/prereqs.jsx b/auth4genai/snippets/how-tos/google-calendar/prereqs.jsx new file mode 100644 index 000000000..17f12086d --- /dev/null +++ b/auth4genai/snippets/how-tos/google-calendar/prereqs.jsx @@ -0,0 +1,115 @@ +export const GoogleCalendarPrereqs = ({ + lang, + createCustomApiClientStep = false, +}) => { + const languageSteps = []; + + if (lang === "js") { + languageSteps.push( + + Install Node.js 20+ and npm + + } + />, + ); + } else if (lang === "python") { + languageSteps.push( + + Install Python 3.11+ and pip + + } + /> + ); + } + + const commonSteps = [ + + Complete the{" "} + + User authentication quickstart + {" "} + to create an application integrated with Auth0. + + } + />, + + + Set up an OpenAI API key + + . + + } + />, + +
    +
  • + Enable the{" "} + + Google Calendar API + + . +
  • +
  • + Create OAuth 2.0 credentials (Web Application) with proper redirect + URIs. +
  • +
+
, + + Configure a{" "} + + Social Connection for Google in Auth0 + + + } + > +
    +
  • + Under the Purpose section, make sure to enable the{" "} + Use for Connected Accounts with Token Vault toggle. +
  • +
  • + Under the Permissions section, enable the Offline Access{" "} + scope. +
  • +
+
, + ]; + + if (createCustomApiClientStep) { + const step = CustomApiClient({ apiName: "Google Calendar API" }); + commonSteps.push(step); + } + + return ( + <> + + Prerequisites + + Before getting started, make sure you have completed the following steps: + {[...languageSteps, ...commonSteps]} + + ); +}; diff --git a/auth4genai/snippets/how-tos/salesforce/prereqs.jsx b/auth4genai/snippets/how-tos/salesforce/prereqs.jsx new file mode 100644 index 000000000..1fb587e92 --- /dev/null +++ b/auth4genai/snippets/how-tos/salesforce/prereqs.jsx @@ -0,0 +1,139 @@ +export const SalesforcePrereqs = ({ + lang, + createCustomApiClientStep = false, +}) => { + const languageSteps = []; + + if (lang === "js") { + languageSteps.push( + + Install Node.js 20+ and npm + + } + /> + ); + } else if (lang === "python") { + languageSteps.push( + + Install Python 3.11+ and pip + + } + /> + ); + } + + const commonSteps = [ + + Complete the{" "} + + User authentication quickstart + {" "} + to create an application integrated with Auth0. + + } + />, + + + Set up an OpenAI API key + + . + + } + />, + + Set up and configure a Salesforce instance with an{" "} + + External Client App + + . + + } + />, + + Set the SALESFORCE_INSTANCE_URL in your .env{" "} + file + + } + > +
+        
+          SALESFORCE_INSTANCE_URL=https://your-instance.salesforce.com
+        
+      
+
, + + + Configure a Salesforce OIDC connection + {" "} + in Auth0. + + } + > +
    +
  • + Under the General section, ensure the following{" "} + Scopes: +
      +
    • + openid +
    • +
    • + api +
    • +
    • + refresh_token +
    • +
    • + offline_access +
    • +
    +
  • +
  • + Under the Purpose section, make sure to enable the{" "} + Use for Connected Accounts with Token Vault toggle. +
  • +
+
, + ]; + + if (createCustomApiClientStep) { + const step = CustomApiClient({ apiName: "Salesforce API" }); + commonSteps.push(step); + } + + return ( + <> + + Prerequisites + + Before getting started, make sure you have completed the following steps: + {[...languageSteps, ...commonSteps]} + + ); +}; diff --git a/auth4genai/snippets/how-tos/slack/cloudflare-agents.mdx b/auth4genai/snippets/how-tos/slack/cloudflare-agents.mdx index bb11619b7..3db5cd91d 100644 --- a/auth4genai/snippets/how-tos/slack/cloudflare-agents.mdx +++ b/auth4genai/snippets/how-tos/slack/cloudflare-agents.mdx @@ -1,3 +1,5 @@ +import ConfigureCloudflareKvStore from "/snippets/common/configure-cloudflare-kv-store.mdx"; + ### 1. Configure Auth0 AI If you started from the [Auth0 Cloudflare Agents starter kit](https://github.com/auth0-lab/cloudflare-agents-starter), you can skip this step as the Auth0 AI SDK is already configured. @@ -17,7 +19,7 @@ npm install @auth0/ai-vercel @auth0/ai-cloudflare @auth0/ai Then, you need to initialize Auth0 AI and set up the connection to request access tokens with the required GitHub scopes. -```typescript ./src/lib/auth0-ai.ts wrap lines +```typescript ./src/agent/auth0-ai.ts wrap lines import { Auth0AI, setGlobalAIContext } from "@auth0/ai-vercel"; import { getCurrentAgent } from "agents"; import type { Chat } from "./chat"; @@ -34,13 +36,15 @@ setGlobalAIContext(() => ({ threadID: getAgent().name })); const auth0AI = new Auth0AI(); +const refreshToken = async () => { + const credentials = getAgent().getCredentials(); + return credentials?.refresh_token; +}; + export const withSlack = auth0AI.withTokenVault({ + refreshToken, connection: "sign-in-with-slack", scopes: ["channels:read", "groups:read"], - refreshToken: async () => { - const credentials = getAgent().getCredentials(); - return credentials?.refresh_token; - }, }); ``` @@ -48,18 +52,19 @@ export const withSlack = auth0AI.withTokenVault({ Wrap your tool using the Auth0 AI SDK to obtain an access token for the Slack API. -```typescript ./src/agent/tools/listRepositories.ts wrap lines highlight={2-4,8,14,18,30-32} -import { ErrorCode, WebClient } from "@slack/web-api"; +```typescript ./src/agent/tools/listRepositories.ts wrap lines highlight={4-5,9,15,19,31-33} +import { tool } from "ai"; +import { z } from "zod/v3"; + import { getAccessTokenFromTokenVault } from "@auth0/ai-vercel"; import { TokenVaultError } from "@auth0/ai/interrupts"; -import { withSlack } from "@/lib/auth0-ai"; -import { tool } from "ai"; -import { z } from "zod"; +import { withSlack } from "@/agent/auth0-ai"; +import { ErrorCode, WebClient } from "@slack/web-api"; export const listChannels = withSlack( tool({ description: "List channels for the current user on Slack", - parameters: z.object({}), + inputSchema: z.object({}), execute: async () => { // Get the access token from Auth0 AI const accessToken = getAccessTokenFromTokenVault(); @@ -79,7 +84,7 @@ export const listChannels = withSlack( if (error && typeof error === "object" && "code" in error) { if (error.code === ErrorCode.HTTPError) { throw new TokenVaultError( - `Authorization required to access the Token Vault connection` + `Authorization required to access the Token Vault` ); } } @@ -101,80 +106,134 @@ Interrupts are a way for the system to pause execution and prompt the user to ta On the Chat agent class, you need to set up the tool invocation and handle the interruption messaging via the `errorSerializer`. -```typescript ./src/agent/chat.ts wrap lines highlight={1-2,52-54,63-66} -import { setAIContext } from "@auth0/ai-vercel"; -import { errorSerializer, withInterruptions } from "@auth0/ai-vercel/interrupts"; -// Other dependencies -import { AuthAgent, OwnedAgent } from "@auth0/auth0-cloudflare-agents-api"; +```typescript ./src/agent/chat.ts wrap lines highlight={1-10,52,54-57,101} import { openai } from "@ai-sdk/openai"; +import { + AsyncUserConfirmationResumer, + CloudflareKVStore, +} from "@auth0/ai-cloudflare"; +import { + errorSerializer, + invokeTools, + withInterruptions, +} from "@auth0/ai-vercel/interrupts"; +import { AuthAgent, OwnedAgent } from "@auth0/auth0-cloudflare-agents-api"; import { AIChatAgent } from "agents/ai-chat-agent"; +import { + convertToModelMessages, + createUIMessageStream, + createUIMessageStreamResponse, + generateId, + stepCountIs, + streamText, + type UIMessage, +} from "ai"; +import { executions, tools } from "./tools"; +import { processToolCalls } from "./utils"; + +const model = openai("gpt-4o-2024-11-20"); + +class BaseChat extends AIChatAgent {} + +const AuthedChat = AuthAgent(BaseChat); +const OwnedAuthedChat = OwnedAgent(AuthedChat); +const ResumableOwnedAuthedChat = AsyncUserConfirmationResumer(OwnedAuthedChat); + +export class Chat extends ResumableOwnedAuthedChat { + messages: UIMessage[] = []; + + declare mcp?: + | { + unstable_getAITools?: () => Record; + } + | undefined; -const SuperAgent = OwnedAgent(AuthAgent(AIChatAgent)); - -export class Chat extends SuperAgent { - async onChatMessage( - onFinish: StreamTextOnFinishCallback, - options?: { abortSignal?: AbortSignal } - ) { - // Collect all tools, including MCP tools + async onChatMessage() { const allTools = { ...tools, - ...this.mcp.unstable_getAITools(), + ...(this.mcp?.unstable_getAITools?.() ?? {}), }; - const claims = this.getClaims(); - // Create a streaming response that handles both text and tool outputs - const dataStreamResponse = createDataStreamResponse({ - execute: withInterruptions(async (dataStream) => { - // Process any pending tool calls from previous messages - // This handles human-in-the-loop confirmations for tools - const processedMessages = await processToolCalls({ - messages: this.messages, - dataStream, - tools: allTools, - executions, - }); - - // Stream the AI response using GPT-4 - const result = streamText({ - model, - system: `You are a helpful assistant that can do various tasks... -${unstable_getSchedulePrompt({ date: new Date() })} + const claims = this.getClaims?.(); + + const stream = createUIMessageStream({ + originalMessages: this.messages, + execute: withInterruptions( + async ({ writer }) => { + await invokeTools({ + messages: convertToModelMessages(this.messages), + tools: allTools, + }); + + const processed = await processToolCalls({ + messages: this.messages, + dataStream: writer, + tools: allTools, + executions, + }); + + const result = streamText({ + model, + stopWhen: stepCountIs(10), + messages: convertToModelMessages(processed), + system: `You are a helpful assistant that can do various tasks... If the user asks to schedule a task, use the schedule tool to schedule the task. -The name of the user is ${claims?.name ?? "unknown"}. -`, - messages: processedMessages, - tools: allTools, - onFinish: async (args) => { - onFinish( - args as Parameters>[0] - ); - }, - onError: (error) => { - if (!Auth0Interrupt.isInterrupt(error)) { - return; - } - console.error("Error while streaming:", error); - }, - maxSteps: 10, - }); - - // Merge the AI response stream with tool execution outputs - result.mergeIntoDataStream(dataStream); - }), - onError: errorSerializer((err) => { - console.log(err); - return "Oops, an error occured!"; - }), +The name of the user is ${claims?.name ?? "unknown"}.`, + tools: allTools, + onStepFinish: (output) => { + if (output.finishReason === "tool-calls") { + const last = output.content[output.content.length - 1]; + if (last?.type === "tool-error") { + const { toolName, toolCallId, error, input } = last; + const serializableError = { + cause: error, + toolCallId, + toolName, + toolArgs: input, + }; + throw serializableError; + } + } + }, + }); + + writer.merge( + result.toUIMessageStream({ + sendReasoning: true, + }) + ); + }, + { messages: this.messages, tools: allTools } + ), + onError: errorSerializer(), }); - return dataStreamResponse; + return createUIMessageStreamResponse({ stream }); + } + + async executeTask(description: string) { + await this.saveMessages([ + ...this.messages, + { + id: generateId(), + role: "user", + parts: [{ type: "text", text: `Running scheduled task: ${description}` }], + }, + ]); + } + + get auth0AIStore() { + return new CloudflareKVStore({ kv: this.env.Session }); } } ``` +**Note about CloudflareKVStore:** + + + #### Client Side In this example, we utilize the `TokenVaultConsentPopup` component to show a pop-up that allows the user to authenticate with GitHub and grant access with the requested scopes. You'll first need to install the `@auth0/ai-components` package: diff --git a/auth4genai/snippets/how-tos/slack/prereqs.jsx b/auth4genai/snippets/how-tos/slack/prereqs.jsx new file mode 100644 index 000000000..705899848 --- /dev/null +++ b/auth4genai/snippets/how-tos/slack/prereqs.jsx @@ -0,0 +1,92 @@ +export const SlackPrereqs = ({ + lang, + createCustomApiClientStep = false, +}) => { + const languageSteps = []; + + if (lang === "js") { + languageSteps.push( + + Install Node.js 20+ and npm + + } + /> + ); + } else if (lang === "python") { + languageSteps.push( + + Install Python 3.11+ and pip + + } + /> + ); + } + + const commonSteps = [ + + Complete the{" "} + + User authentication quickstart + {" "} + to create an application integrated with Auth0. + + } + />, + + + Set up an OpenAI API key + + . + + } + />, + + Configure a{" "} + + Social Connection for Slack in Auth0 + + + } + > +
    +
  • + Under the Purpose section, make sure to enable the{" "} + Use for Connected Accounts with Token Vault toggle. +
  • +
+
, + ]; + + if (createCustomApiClientStep) { + const step = CustomApiClient({ apiName: "Slack API" }); + commonSteps.push(step); + } + + return ( + <> + + Prerequisites + + Before getting started, make sure you have completed the following steps: + {[...languageSteps, ...commonSteps]} + + ); +}; diff --git a/lychee.toml b/lychee.toml index b353fdaf6..848217ef8 100644 --- a/lychee.toml +++ b/lychee.toml @@ -205,6 +205,9 @@ exclude = [ # Constant Contact KB – currently failing TLS / blocked to bots "^https?://knowledgebase\\.constantcontact\\.com/", + # Sample Salesforce instance URL + "^https://your\\-instance\\.salesforce\\.com/", + # # Known broken or legacy URLs (exact-match) #