diff --git a/bots/bugbuster/bot.definition.ts b/bots/bugbuster/bot.definition.ts index daf1b4b5d26..0eba96e6635 100644 --- a/bots/bugbuster/bot.definition.ts +++ b/bots/bugbuster/bot.definition.ts @@ -20,6 +20,15 @@ export default new sdk.BotDefinition({ .describe('List of recently linted issues'), }), }, + watchedTeams: { + type: 'bot', + schema: sdk.z.object({ + teamKeys: sdk.z + .array(sdk.z.string()) + .title('Team Keys') + .describe('The keys of the teams for which BugBuster should lint issues'), + }), + }, }, __advanced: { useLegacyZuiTransformer: true, diff --git a/bots/bugbuster/src/handlers/issue-processor.ts b/bots/bugbuster/src/handlers/issue-processor.ts index 3dde73f0f25..f6d78d57757 100644 --- a/bots/bugbuster/src/handlers/issue-processor.ts +++ b/bots/bugbuster/src/handlers/issue-processor.ts @@ -2,6 +2,8 @@ import { BotLogger } from '@botpress/sdk' import { Issue } from '@linear/sdk' import { LinearApi } from 'src/utils/linear-utils' import * as linlint from '../linear-lint-issue' +import { listTeams } from './teams-manager' +import { Client } from '.botpress' /** * @returns The corresponding issue, or `undefined` if the issue is not found or not valid. @@ -11,7 +13,9 @@ export async function findIssue( teamKey: string | undefined, logger: BotLogger, eventName: string, - linear: LinearApi + linear: LinearApi, + client: Client, + botId: string ): Promise { if (!issueNumber || !teamKey) { logger.error('Missing issueNumber or teamKey in event payload') @@ -20,7 +24,8 @@ export async function findIssue( logger.info(`Linear issue ${eventName} event received`, `${teamKey}-${issueNumber}`) - if (!linear.isTeam(teamKey) || teamKey !== 'SQD') { + const teams = await listTeams(client, botId) + if (!linear.isTeam(teamKey) || !teams.result?.includes(teamKey)) { logger.error(`Ignoring issue of team "${teamKey}"`) return } diff --git a/bots/bugbuster/src/handlers/linear-issue-created.ts b/bots/bugbuster/src/handlers/linear-issue-created.ts index 87823b5849b..4a437224044 100644 --- a/bots/bugbuster/src/handlers/linear-issue-created.ts +++ b/bots/bugbuster/src/handlers/linear-issue-created.ts @@ -5,7 +5,7 @@ import * as bp from '.botpress' export const handleLinearIssueCreated: bp.EventHandlers['linear:issueCreated'] = async (props) => { const { number: issueNumber, teamKey } = props.event.payload const linear = await utils.linear.LinearApi.create() - const issue = await findIssue(issueNumber, teamKey, props.logger, 'created', linear) + const issue = await findIssue(issueNumber, teamKey, props.logger, 'created', linear, props.client, props.ctx.botId) if (!issue) { return diff --git a/bots/bugbuster/src/handlers/linear-issue-updated.ts b/bots/bugbuster/src/handlers/linear-issue-updated.ts index 5a7702c7690..370655f9330 100644 --- a/bots/bugbuster/src/handlers/linear-issue-updated.ts +++ b/bots/bugbuster/src/handlers/linear-issue-updated.ts @@ -5,7 +5,7 @@ import * as bp from '.botpress' export const handleLinearIssueUpdated: bp.EventHandlers['linear:issueUpdated'] = async (props) => { const { number: issueNumber, teamKey } = props.event.payload const linear = await utils.linear.LinearApi.create() - const issue = await findIssue(issueNumber, teamKey, props.logger, 'updated', linear) + const issue = await findIssue(issueNumber, teamKey, props.logger, 'updated', linear, props.client, props.ctx.botId) if (!issue) { return diff --git a/bots/bugbuster/src/handlers/message-created.ts b/bots/bugbuster/src/handlers/message-created.ts index 31115cf2d71..d5a66c0ac87 100644 --- a/bots/bugbuster/src/handlers/message-created.ts +++ b/bots/bugbuster/src/handlers/message-created.ts @@ -1,14 +1,61 @@ import * as utils from '../utils' +import { addTeam, listTeams, removeTeam } from './teams-manager' import * as bp from '.botpress' const MESSAGING_INTEGRATIONS = ['telegram', 'slack'] +const COMMAND_LIST_MESSAGE = `Unknown command. Here's a list of possible commands: +/addTeam [teamName] +/removeTeam [teamName] +/listTeams` +const ARGUMENT_REQUIRED_MESSAGE = 'Error: an argument is required with this command.' export const handleMessageCreated: bp.MessageHandlers['*'] = async (props) => { - const { conversation, message } = props + const { conversation, message, client, ctx } = props if (!MESSAGING_INTEGRATIONS.includes(conversation.integration)) { props.logger.info(`Ignoring message from ${conversation.integration}`) return } + const botpress = await utils.botpress.BotpressApi.create(props) - await botpress.respondText(message.conversationId, "Hey, I'm BugBuster.") + + if (message.type !== 'text') { + await botpress.respondText(conversation.id, COMMAND_LIST_MESSAGE) + return + } + + const [command, teamKey] = message.payload.text.trim().split(' ') + if (!command) { + await botpress.respondText(conversation.id, COMMAND_LIST_MESSAGE) + } + + switch (command) { + case '/addTeam': { + if (!teamKey) { + await botpress.respondText(conversation.id, ARGUMENT_REQUIRED_MESSAGE) + return + } + const linear = await utils.linear.LinearApi.create() + const result = await addTeam(client, ctx.botId, teamKey, linear) + await botpress.respondText(conversation.id, result.message) + break + } + case '/removeTeam': { + if (!teamKey) { + await botpress.respondText(conversation.id, ARGUMENT_REQUIRED_MESSAGE) + return + } + const result = await removeTeam(client, ctx.botId, teamKey) + await botpress.respondText(conversation.id, result.message) + break + } + case '/listTeams': { + const result = await listTeams(client, ctx.botId) + await botpress.respondText(conversation.id, result.message) + break + } + default: { + await botpress.respondText(conversation.id, COMMAND_LIST_MESSAGE) + break + } + } } diff --git a/bots/bugbuster/src/handlers/teams-manager.ts b/bots/bugbuster/src/handlers/teams-manager.ts new file mode 100644 index 00000000000..c919db383a2 --- /dev/null +++ b/bots/bugbuster/src/handlers/teams-manager.ts @@ -0,0 +1,89 @@ +import { LinearApi } from 'src/utils/linear-utils' +import * as bp from '.botpress' + +export type Result = { + success: boolean + message: string + result?: T +} + +const _getWatchedTeams = async (client: bp.Client, botId: string) => { + return ( + await client.getOrSetState({ + id: botId, + name: 'watchedTeams', + type: 'bot', + payload: { + teamKeys: [], + }, + }) + ).state.payload.teamKeys +} + +const _setWatchedTeams = async (client: bp.Client, botId: string, teamKeys: string[]) => { + await client.setState({ + id: botId, + name: 'watchedTeams', + type: 'bot', + payload: { + teamKeys, + }, + }) +} + +export async function addTeam(client: bp.Client, botId: string, key: string, linear: LinearApi): Promise> { + const teamKeys = await _getWatchedTeams(client, botId) + if (teamKeys.includes(key)) { + return { + success: false, + message: `Error: the team with the key '${key}' is already being watched.`, + } + } + if (!linear.isTeam(key)) { + return { + success: false, + message: `Error: the team with the key '${key}' does not exist.`, + } + } + + await _setWatchedTeams(client, botId, [...teamKeys, key]) + return { + success: true, + message: `Success: the team with the key '${key}' has been added to the watched team list.`, + } +} + +export async function removeTeam(client: bp.Client, botId: string, key: string): Promise> { + const teamKeys = await _getWatchedTeams(client, botId) + if (!teamKeys.includes(key)) { + return { + message: `Error: the team with the key '${key}' is not currently being watched.`, + success: false, + } + } + + await _setWatchedTeams( + client, + botId, + teamKeys.filter((k) => k !== key) + ) + return { + success: false, + message: `Success: the team with the key '${key}' has been removed from the watched team list.`, + } +} + +export async function listTeams(client: bp.Client, botId: string): Promise> { + const teamKeys = await _getWatchedTeams(client, botId) + if (teamKeys.length === 0) { + return { + success: false, + message: 'You have no watched teams.', + } + } + return { + success: true, + message: teamKeys.join(', '), + result: teamKeys, + } +} diff --git a/integrations/browser/integration.definition.ts b/integrations/browser/integration.definition.ts index 89ab762eab3..fe070e2d53b 100644 --- a/integrations/browser/integration.definition.ts +++ b/integrations/browser/integration.definition.ts @@ -1,22 +1,24 @@ -/* bplint-disable */ import { IntegrationDefinition } from '@botpress/sdk' import { actionDefinitions } from 'src/definitions/actions' export default new IntegrationDefinition({ name: 'browser', title: 'Browser', - version: '0.8.0', + version: '0.8.1', description: 'Capture screenshots and retrieve web page content with metadata for automated browsing and data extraction.', readme: 'hub.md', icon: 'icon.svg', actions: actionDefinitions, secrets: { - SCREENSHOT_API_KEY: {}, - FIRECRAWL_API_KEY: {}, - LOGO_API_KEY: {}, - }, - __advanced: { - useLegacyZuiTransformer: true, + SCREENSHOT_API_KEY: { + description: 'ScreenShot key', + }, + FIRECRAWL_API_KEY: { + description: 'FireCrawl key', + }, + LOGO_API_KEY: { + description: 'Logo key', + }, }, }) diff --git a/integrations/browser/src/actions/browse-pages.ts b/integrations/browser/src/actions/browse-pages.ts index e46ecd47c34..c6d2abab054 100644 --- a/integrations/browser/src/actions/browse-pages.ts +++ b/integrations/browser/src/actions/browse-pages.ts @@ -27,7 +27,6 @@ const getPageContent = async (props: { try { const result = await firecrawl.scrape(props.url, { - fastMode: true, onlyMainContent: true, maxAge: 60 * 60 * 24 * 7, // 1 week removeBase64Images: true, diff --git a/integrations/browser/src/definitions/actions.ts b/integrations/browser/src/definitions/actions.ts index f4644825311..fea43c686ef 100644 --- a/integrations/browser/src/definitions/actions.ts +++ b/integrations/browser/src/definitions/actions.ts @@ -7,20 +7,24 @@ const captureScreenshot: ActionDefinition = { description: 'Capture a screenshot of the specified page.', input: { schema: z.object({ - url: z.string(), + url: z.string().describe('The url to screenshot').title('Url'), javascriptToInject: multiLineString .optional() - .describe('JavaScript code to inject into the page before taking the screenshot'), - cssToInject: multiLineString.optional().describe('CSS code to inject into the page before taking the screenshot'), - width: z.number().default(1080), - height: z.number().default(1920), - fullPage: z.boolean().default(true), + .describe('JavaScript code to inject into the page before taking the screenshot') + .title('Javascript to Inject'), + cssToInject: multiLineString + .optional() + .describe('CSS code to inject into the page before taking the screenshot') + .title('CSS To Inject'), + width: z.number().default(1080).describe('The width of the screenshot').title('Width'), + height: z.number().default(1920).describe('The height of the screenshot').title('Height'), + fullPage: z.boolean().default(true).describe('Whether the screenshot is fullscreen or not').title('Full Page'), }), }, output: { schema: z.object({ - imageUrl: z.string().describe('URL to the captured screenshot'), - htmlUrl: z.string().optional().describe('URL to the HTML page of the screenshot'), + imageUrl: z.string().describe('URL to the captured screenshot').title('Image Url'), + htmlUrl: z.string().optional().describe('URL to the HTML page of the screenshot').title('Html Url'), }), }, cacheable: true, @@ -43,17 +47,22 @@ const getWebsiteLogo: ActionDefinition = { description: 'Get the logo of the specified website.', input: { schema: z.object({ - domain: z.string().describe('The domain of the website to get the logo from (eg. "example.com")'), - greyscale: z.boolean().default(false).describe('Whether to return the logo in grayscale (black & white)'), + domain: z.string().describe('The domain of the website to get the logo from (eg. "example.com")').title('Domain'), + greyscale: z + .boolean() + .default(false) + .describe('Whether to return the logo in grayscale (black & white)') + .title('Grayscale'), size: z .enum(['64', '128', '256', '512']) .default('128') - .describe('Size of the logo to return (64, 128 or 256, 512 pixels)'), + .describe('Size of the logo to return (64, 128 or 256, 512 pixels)') + .title('Size'), }), }, output: { schema: z.object({ - logoUrl: z.string().describe('URL to the website logo'), + logoUrl: z.string().describe('URL to the website logo').title('Logo Url'), }), }, cacheable: false, @@ -65,25 +74,32 @@ const browsePages: ActionDefinition = { description: 'Extract the full content & the metadata of the specified pages as markdown.', input: { schema: z.object({ - urls: z.array(z.string()), + urls: z.array(z.string()).describe('The list of url to browse').title('Urls'), waitFor: z .number() .optional() .default(350) .describe( 'Time to wait before extracting the content (in milliseconds). Set this value higher for dynamic pages.' - ), - timeout: z.number().optional().default(30000).describe('Timeout for the request (in milliseconds)'), + ) + .title('Wait For'), + timeout: z + .number() + .optional() + .default(30000) + .describe('Timeout for the request (in milliseconds)') + .title('Time Out'), maxAge: z .number() .optional() .default(60 * 60 * 24 * 7) - .describe('Maximum age of the cached page content (in seconds)'), + .describe('Maximum age of the cached page content (in seconds)') + .title('Max Age'), }), }, output: { schema: z.object({ - results: z.array(fullPage), + results: z.array(fullPage).describe('The list of pages browsed'), }), }, cacheable: true, @@ -102,49 +118,56 @@ const webSearch: ActionDefinition = { description: 'Search information on the web. You need to browse to that page to get the full content of the page.', input: { schema: z.object({ - query: z.string().min(1).max(1000).describe('What are we searching for?'), + query: z.string().min(1).max(1000).describe('What are we searching for?').title('Query'), includeSites: z .array(domainNameValidator) .max(20) .optional() - .describe('Include only these domains in the search (max 20)'), + .describe('Include only these domains in the search (max 20)') + .title('Include Site'), excludeSites: z .array(domainNameValidator) .max(20) .optional() - .describe('Exclude these domains from the search (max 20)'), + .describe('Exclude these domains from the search (max 20)') + .title('Exclude Site'), count: z .number() .min(1) .max(20) .optional() .default(10) - .describe('Number of search results to return (default: 10)'), + .describe('Number of search results to return (default: 10)') + .title('Count'), freshness: z .enum(['Day', 'Week', 'Month']) .optional() - .describe('Only consider results from the last day, week or month'), + .describe('Only consider results from the last day, week or month') + .title('Freshness'), browsePages: z .boolean() .optional() .default(false) - .describe('Whether to browse to the pages to get the full content'), + .describe('Whether to browse to the pages to get the full content') + .title('Browse Pages'), }), }, output: { schema: z.object({ - results: z.array( - z.object({ - name: z.string().describe('Title of the page'), - url: z.string().describe('URL of the page'), - snippet: z.string().describe('A short summary of the page'), - links: z - .array(z.object({ name: z.string(), url: z.string() })) - .optional() - .describe('Useful links on the page'), - page: fullPage.optional(), - }) - ), + results: z + .array( + z.object({ + name: z.string().describe('Title of the page'), + url: z.string().describe('URL of the page'), + snippet: z.string().describe('A short summary of the page'), + links: z + .array(z.object({ name: z.string(), url: z.string() })) + .optional() + .describe('Useful links on the page'), + page: fullPage.optional().describe('The page itself'), + }) + ) + .describe('Results of the search'), }), }, billable: true, @@ -166,13 +189,15 @@ const discoverUrls: ActionDefinition = { .string() .describe( 'The URL of the website to discover URLs from. Can be a domain like example.com or a full URL like sub.example.com/page' - ), - onlyHttps: z.boolean().default(true).describe('Whether to only include HTTPS pages'), - count: z.number().min(1).max(10_000).default(5_000), + ) + .title('Url'), + onlyHttps: z.boolean().default(true).describe('Whether to only include HTTPS pages').title('Only HTTPS'), + count: z.number().min(1).max(10_000).default(5_000).describe('The number of urls').title('Count'), include: z .array(globPattern) .max(100, 'You can include up to 100 URL patterns') .describe('List of glob patterns to include URLs from the discovery') + .title('Include') .optional(), exclude: z .array(globPattern) @@ -180,16 +205,18 @@ const discoverUrls: ActionDefinition = { .optional() .describe( 'List of glob patterns to exclude URLs from the discovery. All URLs matching these patterns will be excluded from the results, even if they are included in the "include" patterns.' - ), + ) + .title('Exclude'), }), }, output: { schema: z.object({ - urls: z.array(z.string()).describe('List of discovered URLs'), - excluded: z.number().describe('Number of URLs excluded due to robots.txt or filter'), + urls: z.array(z.string()).describe('List of discovered URLs').title('Urls'), + excluded: z.number().describe('Number of URLs excluded due to robots.txt or filter').title('Excluded'), stopReason: z .enum(['urls_limit_reached', 'end_of_results', 'time_limit_reached']) - .describe('Reason for stopping the URLs discovery. '), + .describe('Reason for stopping the URLs discovery. ') + .title('Stop Reason'), }), }, billable: true, diff --git a/integrations/cerebras/integration.definition.ts b/integrations/cerebras/integration.definition.ts index 7ca7bbafe38..37d81dc2659 100644 --- a/integrations/cerebras/integration.definition.ts +++ b/integrations/cerebras/integration.definition.ts @@ -1,4 +1,3 @@ -/* bplint-disable */ import { IntegrationDefinition, z } from '@botpress/sdk' import { modelId } from 'src/schemas' import llm from './bp_modules/llm' @@ -8,7 +7,7 @@ export default new IntegrationDefinition({ title: 'Cerebras', description: 'Get access to a curated list of Cerebras models for content generation and chat completions within your bot.', - version: '8.0.0', + version: '8.0.1', readme: 'hub.md', icon: 'icon.svg', entities: { @@ -23,9 +22,6 @@ export default new IntegrationDefinition({ description: 'Cerebras API key', }, }, - __advanced: { - useLegacyZuiTransformer: true, - }, }).extend(llm, ({ entities: { modelRef } }) => ({ entities: { modelRef }, })) diff --git a/integrations/whatsapp/integration.definition.ts b/integrations/whatsapp/integration.definition.ts index efdf281054d..07e57d2c745 100644 --- a/integrations/whatsapp/integration.definition.ts +++ b/integrations/whatsapp/integration.definition.ts @@ -95,7 +95,7 @@ const defaultBotPhoneNumberId = { export const INTEGRATION_NAME = 'whatsapp' export default new IntegrationDefinition({ name: INTEGRATION_NAME, - version: '4.5.14', + version: '4.5.15', title: 'WhatsApp', description: 'Send and receive messages through WhatsApp.', icon: 'icon.svg', diff --git a/integrations/whatsapp/src/webhook/handlers/messages.ts b/integrations/whatsapp/src/webhook/handlers/messages.ts index 7619f21a55e..a7c87477ece 100644 --- a/integrations/whatsapp/src/webhook/handlers/messages.ts +++ b/integrations/whatsapp/src/webhook/handlers/messages.ts @@ -82,12 +82,13 @@ async function _handleIncomingMessage( replyTo, }: ValueOf & { incomingMessageType?: string; replyTo?: string }) => { logger.forBot().debug(`Received ${incomingMessageType ?? type} message from WhatsApp:`, payload) - return client.createMessage({ + return client.getOrCreateMessage({ tags: { id: message.id, replyTo }, type, payload, userId: user.id, conversationId: conversation.id, + discriminateByTags: ['id'], }) } diff --git a/packages/cognitive/src/schemas.gen.ts b/packages/cognitive/src/schemas.gen.ts index def9a29f4d4..ab85fd62de4 100644 --- a/packages/cognitive/src/schemas.gen.ts +++ b/packages/cognitive/src/schemas.gen.ts @@ -31,7 +31,7 @@ export type GenerateContentInput = { type: 'text' | 'image' /** Indicates the MIME type of the content. If not provided it will be detected from the content-type header of the provided URL. */ mimeType?: string - /** Required if part type is "text" */ + /** Required if part type is "text" */ text?: string /** Required if part type is "image" */ url?: string @@ -103,7 +103,7 @@ export type GenerateContentOutput = { type: 'text' | 'image' /** Indicates the MIME type of the content. If not provided it will be detected from the content-type header of the provided URL. */ mimeType?: string - /** Required if part type is "text" */ + /** Required if part type is "text" */ text?: string /** Required if part type is "image" */ url?: string