UNPKG

chat

Version:

Unified chat abstraction for Slack, Teams, Google Chat, and Discord

501 lines (494 loc) 17.2 kB
import { toAiMessages } from "../chunk-HD375J7S.js"; // src/ai/tools/channels.ts import { tool } from "ai"; import { z } from "zod"; var getChannelInfo = (chat) => tool({ description: "Fetch metadata for a channel: name, member count, DM status, visibility, etc. Use to identify a channel before posting.", inputSchema: z.object({ channelId: z.string().describe("Full channel id including adapter prefix") }), execute: async ({ channelId }) => { const channel = chat.channel(channelId); const info = await channel.fetchMetadata(); return { id: info.id, name: info.name, isDM: info.isDM ?? false, memberCount: info.memberCount, channelVisibility: info.channelVisibility }; } }); // src/ai/tools/messages.ts import { tool as tool2 } from "ai"; import { z as z2 } from "zod"; var POSTABLE_INPUT = z2.union([ z2.string().describe("Plain text body"), z2.object({ markdown: z2.string() }).describe("Markdown body, converted to the platform's native format"), z2.object({ raw: z2.string() }).describe("Raw body, passed through to the platform untouched") ]).describe("Message body"); var postMessage = (chat, { needsApproval = true } = {}) => tool2({ description: "Post a message inside an existing thread. Use this to reply within a conversation the bot already has context for. The threadId is the full id (e.g. 'slack:C123:1234567890.123456').", needsApproval, inputSchema: z2.object({ threadId: z2.string().describe("Full thread id including adapter prefix"), message: POSTABLE_INPUT }), execute: async ({ threadId, message }) => { const thread = chat.thread(threadId); const sent = await thread.post(toPostable(message)); return { messageId: sent.id, threadId: sent.threadId }; } }); var postChannelMessage = (chat, { needsApproval = true } = {}) => tool2({ description: "Post a top-level message to a channel (not threaded under an existing message). The channelId is the full id (e.g. 'slack:C123ABC').", needsApproval, inputSchema: z2.object({ channelId: z2.string().describe("Full channel id including adapter prefix"), message: POSTABLE_INPUT }), execute: async ({ channelId, message }) => { const channel = chat.channel(channelId); const sent = await channel.post(toPostable(message)); return { messageId: sent.id, threadId: sent.threadId }; } }); var sendDirectMessage = (chat, { needsApproval = true } = {}) => tool2({ description: "Open (or reuse) a 1:1 direct-message conversation with a user and post a message in it. The userId format is platform-specific (e.g. 'U123456' for Slack, 'users/123' for Google Chat).", needsApproval, inputSchema: z2.object({ userId: z2.string().describe("Platform-specific user id; the adapter is auto-detected"), message: POSTABLE_INPUT }), execute: async ({ userId, message }) => { const dm = await chat.openDM(userId); const sent = await dm.post(toPostable(message)); return { messageId: sent.id, threadId: sent.threadId }; } }); var editMessage = (chat, { needsApproval = true } = {}) => tool2({ description: "Edit a previously posted message in a thread. Replaces the existing message body. Only messages the bot itself authored can be edited on most platforms.", needsApproval, inputSchema: z2.object({ threadId: z2.string().describe("Full thread id"), messageId: z2.string().describe("Platform-specific message id of the message to edit"), message: POSTABLE_INPUT }), execute: async ({ threadId, messageId, message }) => { const thread = chat.thread(threadId); const result = await thread.adapter.editMessage( threadId, messageId, toPostable(message) ); return { messageId: result.id, threadId: result.threadId }; } }); var deleteMessage = (chat, { needsApproval = true } = {}) => tool2({ description: "Delete a message from a thread. Only messages the bot itself authored can be deleted on most platforms.", needsApproval, inputSchema: z2.object({ threadId: z2.string().describe("Full thread id"), messageId: z2.string().describe("Platform-specific message id of the message to delete") }), execute: async ({ threadId, messageId }) => { const thread = chat.thread(threadId); await thread.adapter.deleteMessage(threadId, messageId); return { deleted: true, messageId, threadId }; } }); function toPostable(input) { if (typeof input === "string") { return input; } if ("markdown" in input) { return { markdown: input.markdown }; } return { raw: input.raw }; } // src/ai/tools/reactions.ts import { tool as tool3 } from "ai"; import { z as z3 } from "zod"; var addReaction = (chat, { needsApproval = true } = {}) => tool3({ description: "Add an emoji reaction to a specific message. Use a well-known emoji name (e.g. 'thumbs_up', 'heart', 'check') or a platform-native shorthand.", needsApproval, inputSchema: z3.object({ threadId: z3.string().describe("Full thread id"), messageId: z3.string().describe("Platform-specific message id to react to"), emoji: z3.string().describe( "Emoji name or platform shortcode (e.g. 'thumbs_up', 'white_check_mark')" ) }), execute: async ({ threadId, messageId, emoji }) => { const thread = chat.thread(threadId); await thread.adapter.addReaction(threadId, messageId, emoji); return { added: true, emoji, messageId, threadId }; } }); var removeReaction = (chat, { needsApproval = true } = {}) => tool3({ description: "Remove an emoji reaction the bot previously added to a message.", needsApproval, inputSchema: z3.object({ threadId: z3.string().describe("Full thread id"), messageId: z3.string().describe("Platform-specific message id to remove the reaction from"), emoji: z3.string().describe( "Emoji name or platform shortcode previously added by the bot" ) }), execute: async ({ threadId, messageId, emoji }) => { const thread = chat.thread(threadId); await thread.adapter.removeReaction(threadId, messageId, emoji); return { removed: true, emoji, messageId, threadId }; } }); // src/ai/tools/threads.ts import { tool as tool4 } from "ai"; import { z as z4 } from "zod"; var FETCH_DIRECTION = z4.enum(["forward", "backward"]).optional().default("backward"); function projectMessage(message) { return { id: message.id, threadId: message.threadId, text: message.text, author: { userId: message.author.userId, userName: message.author.userName, fullName: message.author.fullName, isBot: message.author.isBot, isMe: message.author.isMe }, dateSent: message.metadata.dateSent?.toISOString(), edited: message.metadata.edited, isMention: message.isMention, attachments: (message.attachments ?? []).map((att) => ({ type: att.type, name: att.name, mimeType: att.mimeType, url: att.url })) }; } var fetchMessages = (chat) => tool4({ description: "Fetch recent messages from a thread, ordered chronologically (oldest first within the page). Use to read the conversation before responding.", inputSchema: z4.object({ threadId: z4.string().describe("Full thread id"), limit: z4.number().int().min(1).max(100).optional().default(20).describe("Maximum number of messages to fetch"), cursor: z4.string().optional().describe("Pagination cursor from a previous fetchMessages call"), direction: FETCH_DIRECTION.describe( "'backward' (default) returns the most recent messages; 'forward' iterates from the oldest" ) }), execute: async ({ threadId, limit, cursor, direction }) => { const thread = chat.thread(threadId); const result = await thread.adapter.fetchMessages(threadId, { limit, cursor, direction }); return { messages: result.messages.map(projectMessage), nextCursor: result.nextCursor }; } }); var fetchChannelMessages = (chat) => tool4({ description: "Fetch top-level messages in a channel (not thread replies). Returns messages in chronological order within the page.", inputSchema: z4.object({ channelId: z4.string().describe("Full channel id"), limit: z4.number().int().min(1).max(100).optional().default(20), cursor: z4.string().optional(), direction: FETCH_DIRECTION }), execute: async ({ channelId, limit, cursor, direction }) => { const adapterName = channelId.split(":")[0]; const adapter = adapterName ? chat.getAdapter(adapterName) : void 0; if (!adapter?.fetchChannelMessages) { throw new Error( `Adapter "${adapterName}" does not support fetching channel messages` ); } const result = await adapter.fetchChannelMessages(channelId, { limit, cursor, direction }); return { messages: result.messages.map(projectMessage), nextCursor: result.nextCursor }; } }); var fetchThread = (chat) => tool4({ description: "Fetch metadata about a thread (channel id, channel name, visibility, DM status, etc).", inputSchema: z4.object({ threadId: z4.string().describe("Full thread id") }), execute: async ({ threadId }) => { const thread = chat.thread(threadId); const info = await thread.adapter.fetchThread(threadId); return { id: info.id, channelId: info.channelId, channelName: info.channelName, channelVisibility: info.channelVisibility, isDM: info.isDM ?? false }; } }); var listThreads = (chat) => tool4({ description: "List recent threads in a channel. Returns lightweight summaries with the root message of each thread.", inputSchema: z4.object({ channelId: z4.string().describe("Full channel id"), limit: z4.number().int().min(1).max(100).optional().default(20), cursor: z4.string().optional() }), execute: async ({ channelId, limit, cursor }) => { const adapterName = channelId.split(":")[0]; const adapter = adapterName ? chat.getAdapter(adapterName) : void 0; if (!adapter?.listThreads) { throw new Error( `Adapter "${adapterName}" does not support listing threads` ); } const result = await adapter.listThreads(channelId, { limit, cursor }); return { threads: result.threads.map((t) => ({ id: t.id, replyCount: t.replyCount, lastReplyAt: t.lastReplyAt?.toISOString(), rootMessage: projectMessage(t.rootMessage) })), nextCursor: result.nextCursor }; } }); var getThreadParticipants = (chat) => tool4({ description: "Return the unique non-bot participants in a thread. Useful for deciding whether to subscribe (1:1) or stay quiet (group).", inputSchema: z4.object({ threadId: z4.string().describe("Full thread id") }), execute: async ({ threadId }) => { const thread = chat.thread(threadId); const participants = await thread.getParticipants(); return { participants: participants.map((author) => ({ userId: author.userId, userName: author.userName, fullName: author.fullName, isBot: author.isBot })) }; } }); var subscribeThread = (chat, { needsApproval = true } = {}) => tool4({ description: "Subscribe to all future messages in a thread. After subscribing, the bot will receive every message in this thread (not just @mentions).", needsApproval, inputSchema: z4.object({ threadId: z4.string().describe("Full thread id to subscribe to") }), execute: async ({ threadId }) => { const thread = chat.thread(threadId); await thread.subscribe(); return { subscribed: true, threadId }; } }); var unsubscribeThread = (chat, { needsApproval = true } = {}) => tool4({ description: "Unsubscribe from a thread. The bot will stop receiving non-mention messages in this thread.", needsApproval, inputSchema: z4.object({ threadId: z4.string().describe("Full thread id to unsubscribe from") }), execute: async ({ threadId }) => { const thread = chat.thread(threadId); await thread.unsubscribe(); return { subscribed: false, threadId }; } }); var startTyping = (chat) => tool4({ description: "Show a typing indicator in a thread. Use this when starting a long-running operation so users know the bot is working.", inputSchema: z4.object({ threadId: z4.string().describe("Full thread id"), status: z4.string().optional().describe( "Optional human-readable status (some platforms display this, others ignore it)" ) }), execute: async ({ threadId, status }) => { const thread = chat.thread(threadId); await thread.startTyping(status); return { typing: true, threadId }; } }); // src/ai/tools/users.ts import { tool as tool5 } from "ai"; import { z as z5 } from "zod"; var getUser = (chat) => tool5({ description: "Look up profile information about a user by their platform-specific id (e.g. 'U123456' for Slack, '29:...' for Teams, 'users/123' for Google Chat). Returns null if the user is unknown.", inputSchema: z5.object({ userId: z5.string().describe("Platform-specific user id; the adapter is auto-detected") }), execute: async ({ userId }) => { const user = await chat.getUser(userId); if (!user) { return null; } return { userId: user.userId, userName: user.userName, fullName: user.fullName, email: user.email, isBot: user.isBot, avatarUrl: user.avatarUrl }; } }); // src/ai/index.ts var PROTECTED_TOOL_FIELDS = /* @__PURE__ */ new Set([ "args", "execute", "id", "inputSchema", "outputSchema", "supportsDeferredResults", "type" ]); var PRESET_TOOLS = { reader: [ "fetchMessages", "fetchChannelMessages", "fetchThread", "listThreads", "getThreadParticipants", "getChannelInfo", "getUser" ], messenger: [ "fetchMessages", "fetchThread", "getChannelInfo", "getUser", "postMessage", "postChannelMessage", "sendDirectMessage", "addReaction", "removeReaction", "startTyping" ], moderator: [ "fetchMessages", "fetchChannelMessages", "fetchThread", "listThreads", "getThreadParticipants", "getChannelInfo", "getUser", "postMessage", "postChannelMessage", "sendDirectMessage", "editMessage", "deleteMessage", "addReaction", "removeReaction", "subscribeThread", "unsubscribeThread", "startTyping" ] }; function resolveApproval(toolName, config) { if (typeof config === "boolean") { return config; } return config[toolName] ?? true; } function resolvePresetTools(preset) { const presets = Array.isArray(preset) ? preset : [preset]; const tools = /* @__PURE__ */ new Set(); for (const p of presets) { for (const t of PRESET_TOOLS[p]) { tools.add(t); } } return tools; } function applyOverrides(tool6, overrides) { if (!overrides) { return tool6; } const safeOverrides = Object.fromEntries( Object.entries(overrides).filter( ([key]) => !PROTECTED_TOOL_FIELDS.has(key) ) ); return { ...tool6, ...safeOverrides }; } function createChatTools({ chat, requireApproval = true, preset, overrides }) { if (!chat) { throw new Error( "createChatTools requires a `chat` instance. Pass your `new Chat({ ... })` instance as the `chat` option." ); } const approval = (name) => ({ needsApproval: resolveApproval(name, requireApproval) }); const allowed = preset ? resolvePresetTools(preset) : null; const factories = { fetchMessages: () => fetchMessages(chat), fetchChannelMessages: () => fetchChannelMessages(chat), fetchThread: () => fetchThread(chat), listThreads: () => listThreads(chat), getThreadParticipants: () => getThreadParticipants(chat), getChannelInfo: () => getChannelInfo(chat), getUser: () => getUser(chat), startTyping: () => startTyping(chat), postMessage: () => postMessage(chat, approval("postMessage")), postChannelMessage: () => postChannelMessage(chat, approval("postChannelMessage")), sendDirectMessage: () => sendDirectMessage(chat, approval("sendDirectMessage")), editMessage: () => editMessage(chat, approval("editMessage")), deleteMessage: () => deleteMessage(chat, approval("deleteMessage")), addReaction: () => addReaction(chat, approval("addReaction")), removeReaction: () => removeReaction(chat, approval("removeReaction")), subscribeThread: () => subscribeThread(chat, approval("subscribeThread")), unsubscribeThread: () => unsubscribeThread(chat, approval("unsubscribeThread")) }; const entries = Object.entries(factories).filter(([name]) => !allowed || allowed.has(name)).map(([name, build]) => { const built = build(); return [name, applyOverrides(built, overrides?.[name])]; }); return Object.fromEntries(entries); } export { addReaction, createChatTools, deleteMessage, editMessage, fetchChannelMessages, fetchMessages, fetchThread, getChannelInfo, getThreadParticipants, getUser, listThreads, postChannelMessage, postMessage, removeReaction, sendDirectMessage, startTyping, subscribeThread, toAiMessages, unsubscribeThread };