UNPKG

@botpress/adk-cli

Version:

Command-line interface for the Botpress Agent Development Kit (ADK)

855 lines (671 loc) 21.7 kB
# Desk Integration Desk is Botpress's AI-augmented support workspace where human agents and AI bots collaborate on customer tickets. This guide covers how to build ADK agents that participate in the Desk support workflow. ## Quick Start Minimal working example for a Desk support bot: ```typescript // src/conversations/desk.ts import { Autonomous, context, Conversation, z } from '@botpress/runtime' import { actions, joinMarkdownChildren } from '@botpress/runtime/runtime' export default new Conversation({ channel: 'desk.ticket', state: z.object({ lastSync: z.string().default(''), }), events: ['desk:ticketActivity'], handler: async ({ conversation, execute, send }) => { // CRITICAL: Only process tickets assigned to this bot if (conversation.tags['desk:dassignedself'] !== 'true') { console.log('Ticket not assigned to bot, ignoring...') return } await execute({ tools: [], instructions: 'You are a helpful support agent...', }) }, }) ``` ### Configuration (agent.config.ts) ```typescript import { defineConfig } from '@botpress/runtime' export default defineConfig({ name: 'support-bot', dependencies: { integrations: { desk: { enabled: true, configuration: { displayName: 'Support Bot', botHandle: 'SupportBot', canBeAssigned: true, }, }, }, }, }) ``` --- ## Overview ### What is Desk? Desk is a unified support workspace that: - Aggregates tickets from multiple sources (Intercom, Zendesk, native) - Enables human agents and AI bots to collaborate on customer tickets - Provides real-time activity feeds and assignment management - Supports internal notes (staff-only) and customer-visible replies ### Bot Participation Modes | Mode | `canBeAssigned` | Description | | ------------- | --------------- | ------------------------------------------------------ | | **Assigned** | `true` | Bot owns tickets and replies directly to customers | | **Assisting** | `false` | Bot helps human agents with suggestions and automation | ### Multi-Bot Teams Multiple bots can be registered with Desk, each with different capabilities: - A triage bot that classifies and routes tickets - A knowledge bot that searches documentation - A specialist bot for specific product areas Each bot is identified by its `botHandle` (e.g., "@SupportBot", "@TriageBot"). --- ## Configuration All Desk configuration lives in `agent.config.ts` inside the `dependencies.integrations.desk` block: ```typescript import { defineConfig } from '@botpress/runtime' export default defineConfig({ name: 'my-agent', dependencies: { integrations: { desk: { enabled: true, configuration: { displayName: 'My Support Bot', botHandle: 'MySupportBot', displayAvatarUrl: 'https://example.com/bot-avatar.png', canBeAssigned: true, }, }, }, }, }) ``` ### Configuration Options | Option | Type | Required | Description | | ------------------ | ------- | -------- | --------------------------------------------------------------------------------- | | `displayName` | string | Yes | Name shown in Desk UI | | `botHandle` | string | Yes | Handle for @mentions (3-20 chars, starts with letter, alphanumeric + underscores) | | `displayAvatarUrl` | string | No | URL of avatar image | | `canBeAssigned` | boolean | No | Whether bot can be assigned to tickets (default: `false`) | ### Handle Rules The `botHandle` must: - Be 3-20 characters - Start with a letter - Contain only letters, numbers, and underscores Examples: - `SupportBot`, `Triage_Bot`, `KB123` - `Bot`, `123Bot`, `support-bot` --- ## Channel & Events ### Channel: desk.ticket The Desk integration exposes a single channel: `desk.ticket`. All ticket interactions flow through this channel. ```typescript export default new Conversation({ channel: 'desk.ticket', // ... }) ``` ### Event: desk:ticketActivity Emitted when new activities (comments, notes, or system activities) are added to a ticket the bot is assigned to. ```typescript export default new Conversation({ channel: 'desk.ticket', events: ['desk:ticketActivity'], handler: async ({ event }) => { // event.payload contains: // - ticketId: string // - activityCount: number }, }) ``` **Important:** The event does NOT include activities created by the bot itself (prevents infinite loops). ### Conversation Tags | Tag | Description | Example | | ---------------------- | --------------------------- | ------------------------------- | | `desk:did` | Desk ticket ID | `tkt_abc123` | | `desk:status` | Ticket status | `open`, `closed`, `snoozed` | | `desk:title` | Ticket title | `"Login issue"` | | `desk:system` | Source system | `intercom`, `zendesk`, `native` | | `desk:url` | URL to view in Desk | `https://desk.botpress.com/...` | | `desk:snoozeduntil` | Wake time if snoozed | ISO timestamp | | `desk:dtags` | Desk tags (comma-separated) | `urgent,billing` | | `desk:dcustomerid` | Customer ID | `cus_xyz789` | | `desk:dassignedid` | Assigned admin/bot ID | `adm_123`, `ibot_456` | | `desk:dassignedself` | **Is this bot assigned?** | `"true"` or `"false"` | | `desk:dsyncactivityid` | Last synced activity ID | `tka_abc123` | ### Message Tags | Tag | Description | | ------------------ | -------------------------- | | `desk:did` | Activity ID (`tka_...`) | | `desk:dassignedid` | Who was assigned when sent | | `desk:dstatus` | Ticket status when sent | --- ## Message Types The Desk channel supports three message types: ### 1. Reply (text) — Customer-Visible Replies are visible to the customer and may be sent via email. ```typescript await send({ type: 'text', payload: { format: 'markdown', text: 'Hello! How can I help you today?', }, }) ``` **Schema:** ```typescript { format: "plain" | "html" | "markdown", text: string, attachments?: Array<{ id: string, url: string, name: string, contentType: string, size: number, width?: number, height?: number, }> } ``` ### 2. Note — Internal Staff-Only Notes are only visible to support staff (admins and bots), never to customers. ```typescript await send({ type: 'note', payload: { format: 'markdown', text: 'Internal note: Customer has billing issues in CRM', }, }) ``` **Schema:** ```typescript { format: "plain" | "html" | "markdown", text: string, mentions?: Array<{ id: string, type: "admin" | "bot", name: string, }> } ``` ### 3. Activity — System Events Activities represent system events like assignment changes, status changes, etc. ```typescript await send({ type: 'activity', payload: { type: 'ticket.activity', private: true, activity: { type: 'ticket.assigned', assignedTo: { type: 'bot', id: 'ibot_abc123', reply_as: 'SupportBot', }, }, }, }) ``` **Activity Types:** `ticket.snoozed`, `ticket.unsnoozed`, `ticket.assigned`, `ticket.unassigned`, `ticket.closed`, `ticket.reopened`, `ticket.renamed`, `ticket.tags_updated` --- ## Conversation Handler Pattern ### Complete Handler Structure ```typescript import { Autonomous, context, Conversation, z } from '@botpress/runtime' import { actions, joinMarkdownChildren, TranscriptItem } from '@botpress/runtime/runtime' export default new Conversation({ channel: 'desk.ticket', state: z.object({ customer: z.object({ id: z.string(), name: z.string(), email: z.string().optional(), }), admins: z.array(z.object({ id: z.string(), name: z.string(), email: z.string().optional() })), ticket: z.object({ id: z.string(), title: z.string(), status: z.string(), priority: z.string().optional(), assignedTo: z.string().optional(), createdAt: z.string(), }), lastSync: z.string().default(''), }), events: ['desk:ticketActivity'], handler: async ({ type, conversation, message, state, event, execute, client }) => { // ... handler logic }, }) ``` ### Assignment Check Pattern (CRITICAL) **This is the most important pattern.** Without this check, your bot will process ALL tickets in the workspace. ```typescript handler: async ({ conversation }) => { if (conversation.tags['desk:dassignedself'] !== 'true') { console.log('Ticket not assigned to bot, ignoring...') return } // Only process assigned tickets below this line } ``` **Why?** The Desk integration fires events for ALL ticket activity. Without the assignment check: - Your bot processes every ticket in the workspace - Customers receive confusing or duplicate responses - Unnecessary resource and API consumption ### Transcript Management Sync messages to maintain conversation context: ```typescript handler: async ({ conversation, state, client }) => { // Check assignment first... const chat = context.get('chat') const transcript = await chat.fetchTranscript() const messages = await client._inner.list .messages({ conversationId: conversation.id, afterDate: state.lastSync, }) .collect({}) .then((messages) => messages.sort((a, b) => (a.createdAt < b.createdAt ? -1 : 1))) const newMessages = [ ...transcript, ...messages.map( (m) => ({ id: m.id, content: JSON.stringify(m.payload), createdAt: m.createdAt, role: m.direction === 'incoming' ? 'user' : 'assistant', attachments: [], name: m.userId, }) satisfies TranscriptItem ), ] chat.setTranscript(newMessages) state.lastSync = newMessages.filter((x) => x.role === 'user' || x.role === 'assistant').slice(-1)[0]?.createdAt } ``` > **Note:** Use `context.enterWith()` when reassigning context in async operations. Do not use `context.set("chat", ...)` for full context reassignment. --- ## Reply and Note Components For AI-driven responses, register custom components that map to Desk message types. ### Reply Component ```typescript const Reply = new Autonomous.Component({ name: 'Reply', description: 'Sends a reply to the customer, visible to them and via email.', type: 'leaf', leaf: { props: z.object({}) }, aliases: [], examples: [ { name: 'Reply', description: 'Sends a reply to the customer', code: ` yield <Message><Reply> Some **markdown** reply to the customer! </Reply></Message>`, }, ], }) ``` ### Note Component ```typescript const Note = new Autonomous.Component({ name: 'Note', description: 'Adds an internal note to the ticket. Not visible to the customer.', type: 'leaf', leaf: { props: z.object({}) }, aliases: [], examples: [ { name: 'Note', description: 'Adds an internal note to the ticket', code: `yield <Message><Note>This is a **note** with markdown!</Note></Message>`, }, ], }) ``` ### Component Registration ```typescript handler: async ({ conversation }) => { // ... assignment check ... const chat = context.get('chat') // Remove default message components chat.removeComponent('MESSAGE') chat.removeComponent('Text') chat.removeComponent('TEXT') // Register Reply component chat.registerComponent({ component: Reply as any, handler: async ({ props, children }) => { await conversation.send({ type: 'text', payload: { format: 'markdown', text: joinMarkdownChildren(children) }, }) }, }) // Register Note component chat.registerComponent({ component: Note as any, handler: async ({ props, children }) => { await conversation.send({ type: 'note', payload: { format: 'markdown', text: joinMarkdownChildren(children) }, }) }, }) // Update chat context with new components const newChat = Object.assign(chat, { getComponents: async () => [Reply, Note], } satisfies Partial<typeof chat>) context.enterWith({ ...context.getAll(), chat: newChat }) } ``` --- ## Available Actions The Desk integration provides 11 actions for ticket management. ### Ticket Queries ```typescript // listTickets - List tickets with pagination const result = await actions.desk.listTickets({ orderBy: 'updatedAt', orderDirection: 'desc', limit: 50, cursor: undefined, }) // Returns: { tickets: Ticket[], hasMore: boolean, nextCursor?: string } // getTicket - Get a single ticket by ID const result = await actions.desk.getTicket({ id: 'tkt_abc123' }) // listTicketActivities - List activities for a ticket const result = await actions.desk.listTicketActivities({ ticketId: 'tkt_abc123', limit: 50, cursor: undefined, }) ``` ### Assignment Actions ```typescript // assignTicket - Assign to admin or bot await actions.desk.assignTicket({ ticketId: 'tkt_abc123', assigneeId: 'adm_xyz789', // or "ibot_..." for a bot }) // unassignTicket - Remove assignment await actions.desk.unassignTicket({ ticketId: 'tkt_abc123' }) ``` ### Lifecycle Actions ```typescript // closeTicket await actions.desk.closeTicket({ ticketId: 'tkt_abc123' }) // reopenTicket await actions.desk.reopenTicket({ ticketId: 'tkt_abc123' }) // snoozeTicket await actions.desk.snoozeTicket({ ticketId: 'tkt_abc123', snoozedUntil: '2025-01-21T09:00:00Z', }) // unsnoozeTicket await actions.desk.unsnoozeTicket({ ticketId: 'tkt_abc123' }) ``` ### Metadata Actions ```typescript // renameTicket await actions.desk.renameTicket({ ticketId: 'tkt_abc123', title: 'Updated: Login Issue with 2FA', }) // addTags await actions.desk.addTags({ ticketId: 'tkt_abc123', tags: ['urgent', 'billing', 'enterprise'], }) // removeTags await actions.desk.removeTags({ ticketId: 'tkt_abc123', tags: ['low-priority'], }) ``` --- ## Converting Actions to Tools Convert Desk actions to AI-callable tools using `asTool()` and `setStaticInputValues()`: ```typescript handler: async ({ conversation, execute }) => { // Check assignment... const ticketId = conversation.tags['desk:did'] // Create tools with bound ticketId const tools = [ actions.desk.unassignTicket.asTool().setStaticInputValues({ ticketId }), actions.desk.closeTicket.asTool().setStaticInputValues({ ticketId }), actions.desk.snoozeTicket.asTool().setStaticInputValues({ ticketId }), actions.desk.addTags.asTool().setStaticInputValues({ ticketId }), actions.desk.removeTags.asTool().setStaticInputValues({ ticketId }), ] await execute({ tools, instructions: 'You are a helpful support agent...', }) } ``` --- ## AI Execution with Desk ### Execute with Instructions ```typescript handler: async ({ conversation, execute }) => { if (conversation.tags['desk:dassignedself'] !== 'true') return const ticketId = conversation.tags['desk:did'] const UnassignTool = actions.desk.unassignTicket.asTool().setStaticInputValues({ ticketId }) await execute({ iterations: 5, tools: [UnassignTool], model: 'anthropic:claude-sonnet-4-20250514', instructions: `You are a helpful assistant for customer support tickets. You can choose to: - Reply to the customer using <Reply> - Add an internal note using <Note> - Take no action ## Rules - Always use <Reply> for customer-visible messages - Always use <Note> for internal staff communication - Be polite, concise, and don't make up information - Ask for human help if unsure ## Current Ticket ${JSON.stringify(conversation.tags, null, 2)} `, }) } ``` ### System Prompt Guidelines **DO:** - Explain when to use Reply vs Note - Set clear escalation criteria - Include ticket context via tags - Define what "taking no action" means **DON'T:** - Allow direct customer replies without components - Let the bot make up information - Process messages the bot sent itself - Reply to every message (e.g., "thank you") --- ## Escalation Pattern When a bot needs to hand off to a human agent, follow this 4-step pattern: ```typescript async function escalateToHuman(conversation: any, reason: string): Promise<void> { // Step 1: Internal note explaining why await conversation.send({ type: 'note', payload: { format: 'markdown', text: `Escalating to human: ${reason}`, }, }) // Step 2: Customer reply await conversation.send({ type: 'text', payload: { format: 'markdown', text: "I've connected you with a member of our team who can help. They'll be with you shortly!", }, }) // Step 3: Unassign bot await actions.desk.unassignTicket({ ticketId: conversation.tags['desk:did'], }) } // In handler: handler: async ({ conversation }) => { if (conversation.tags['desk:dassignedself'] !== 'true') return if (needsEscalation) { await escalateToHuman(conversation, 'Complex billing issue') return // Step 4: Stop processing } // ... continue normal processing } ``` ### When to Escalate - Technical issues beyond basic troubleshooting - Billing or account access issues - Angry or frustrated customers - Any uncertainty about the correct answer --- ## Data Schemas ### Ticket ```typescript interface Ticket { id: string // "tkt_abc123" title: string customer: Customer priority: 'low' | 'medium' | 'high' | 'urgent' system: 'native' | 'intercom' | 'zendesk' systemTicketId: string assignedTo?: Admin | Bot status: 'open' | 'closed' | 'snoozed' createdAt: string updatedAt: string closedAt?: string snoozedUntil?: string tags: string[] metadata?: Record<string, any> } ``` ### Customer ```typescript interface Customer { id: string // "cus_xyz789" name?: string email?: string phone?: string avatarUrl?: string } ``` ### Admin ```typescript interface Admin { id: string // "adm_abc123" name: string email: string avatarUrl?: string } ``` ### Bot ```typescript interface Bot { id: string // "ibot_def456" displayName: string displayAvatarUrl?: string botHandle: string canBeAssigned: boolean } ``` ### Activity ```typescript interface Activity { id: string // "tka_abc123" ticketId: string activityType: 'comment' | 'note' | 'system_public' | 'system_internal' authorType: 'admin' | 'customer' | 'bot' | 'user' authorId: string body: string bodyFormat: 'plain' | 'html' | 'markdown' | 'json' isRedacted: boolean createdAt: string updatedAt: string } ``` ### Activity Event Types ```typescript type TicketActivityType = | { type: 'ticket.snoozed'; author: Author } | { type: 'ticket.unsnoozed'; author: Author } | { type: 'ticket.assigned'; assignedTo: { type: 'admin' | 'bot'; id: string; reply_as?: string } } | { type: 'ticket.unassigned'; author: Author } | { type: 'ticket.closed'; author: Author } | { type: 'ticket.reopened'; author: Author } | { type: 'ticket.renamed'; title: string } | { type: 'ticket.tags_updated'; author: Author; added: string[]; removed: string[] } ``` --- ## Common Mistakes ### Missing Assignment Check ```typescript // WRONG - Processes ALL tickets handler: async ({ conversation, execute }) => { await execute({ ... }); } // CORRECT - Check assignment first handler: async ({ conversation, execute }) => { if (conversation.tags["desk:dassignedself"] !== "true") return; await execute({ ... }); } ``` ### Using Reply When Should Use Note ```typescript // WRONG - Customer sees internal info await send({ type: 'text', payload: { text: 'Customer has overdue invoices in CRM' } }) // CORRECT - Note for internal communication await send({ type: 'note', payload: { text: 'Customer has overdue invoices in CRM' } }) ``` ### Not Extracting ticketId for Tools ```typescript // WRONG - Tool has no ticketId const UnassignTool = actions.desk.unassignTicket.asTool() // CORRECT - Bind ticketId const ticketId = conversation.tags['desk:did'] const UnassignTool = actions.desk.unassignTicket.asTool().setStaticInputValues({ ticketId }) ``` ### Using Wrong Tag Names ```typescript // WRONG conversation.tags['deskId'] conversation.tags['desk:assignedSelf'] // wrong case // CORRECT conversation.tags['desk:did'] conversation.tags['desk:dassignedself'] // lowercase ``` ### Replying to Every Message ```typescript // WRONG instructions: 'Always reply to every message from the customer' // CORRECT instructions: `You DON'T have to reply to every message. If the customer says "thanks" or "got it", take no action. Only reply when you have something meaningful to add.` ``` --- ## See Also - [./conversations.md](./conversations.md) General conversation handling - [./integration-actions.md](./integration-actions.md) Using integration actions - [./actions.md](./actions.md) Actions and Tools reference - [./tags.md](./tags.md) Tags reference - [./patterns-mistakes.md](./patterns-mistakes.md) General best practices - [./agent-config.md](./agent-config.md) Configuration reference