feat(twilio-block): added twilio block/tools (#201)

* Add twilio block base

* Edit fields

* Add twilio tool

* Fix Serialization of body in request. Add check for form-url-encoded.

* Remove Test file

* Remove execute in types
This commit is contained in:
Ivan Ma
2025-04-01 17:02:15 -07:00
committed by GitHub
parent 6abd26b305
commit be8f44267e
8 changed files with 239 additions and 3 deletions
+75
View File
@@ -0,0 +1,75 @@
import { TwilioIcon } from '@/components/icons'
import { BlockCategory, BlockConfig, BlockIcon } from '../types'
import { TwilioSMSBlockOutput } from '@/tools/twilio/types'
export const TwilioSMSBlock: BlockConfig<TwilioSMSBlockOutput> = {
type: 'twilio_sms',
name: 'Twilio SMS',
description: 'Send SMS messages via Twilio',
longDescription:
'Send text messages to single or multiple recipients using the Twilio API.',
category: 'tools',
bgColor: '#F22F46', // Twilio brand color
icon: TwilioIcon,
subBlocks: [
{
id: 'phoneNumbers',
title: 'Recipient Phone Numbers',
type: 'long-input',
layout: 'full',
placeholder: 'Enter phone numbers with country code (one per line, e.g., +1234567890)',
},
{
id: 'message',
title: 'Message',
type: 'long-input',
layout: 'full',
placeholder: 'e.g. "Hello! This is a test message."',
},
{
id: 'accountSid',
title: 'Twilio Account SID',
type: 'short-input',
layout: 'full',
placeholder: 'Your Twilio Account SID',
},
{
id: 'authToken',
title: 'Auth Token',
type: 'short-input',
layout: 'full',
placeholder: 'Your Twilio Auth Token',
password: true,
},
{
id: 'fromNumber',
title: 'From Twilio Phone Number',
type: 'short-input',
layout: 'full',
placeholder: 'e.g. +1234567890',
}
],
tools: {
access: ['twilio_send_sms'],
config: {
tool: () => 'twilio_send_sms'
},
},
inputs: {
phoneNumbers: { type: 'string', required: true },
message: { type: 'string', required: true },
accountSid: { type: 'string', required: true },
authToken: { type: 'string', required: true },
fromNumber: { type: 'string', required: true }
},
outputs: {
response: {
type: {
success: 'boolean',
messageId: 'string',
status: 'string',
error: 'string'
},
},
},
}
+3
View File
@@ -35,6 +35,7 @@ import { XBlock } from './blocks/x'
import { YouTubeBlock } from './blocks/youtube'
import { AirtableBlock } from './blocks/airtable'
import { BlockConfig } from './types'
import { TwilioSMSBlock } from './blocks/twilio'
// Export blocks for ease of use
export {
@@ -71,6 +72,7 @@ export {
GoogleSheetsBlock,
PerplexityBlock,
ConfluenceBlock,
TwilioSMSBlock,
ImageGeneratorBlock,
TypeformBlock,
}
@@ -107,6 +109,7 @@ const blocks: Record<string, BlockConfig> = {
supabase: SupabaseBlock,
tavily: TavilyBlock,
translate: TranslateBlock,
twilio_sms: TwilioSMSBlock,
typeform: TypeformBlock,
vision: VisionBlock,
whatsapp: WhatsAppBlock,
+20
View File
@@ -1734,6 +1734,26 @@ export function ConfluenceIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function TwilioIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 256 256"
fill="none"
aria-hidden="true"
>
<circle cx="128" cy="128" r="128" fill="none" stroke="white" strokeWidth="21"/>
<circle cx="85" cy="85" r="21" fill="white"/>
<circle cx="171" cy="85" r="21" fill="white"/>
<circle cx="85" cy="171" r="21" fill="white"/>
<circle cx="171" cy="171" r="21" fill="white"/>
</svg>
)
}
export function ImageIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
+2
View File
@@ -44,6 +44,7 @@ import { visionTool } from './vision/vision'
import { whatsappSendMessageTool } from './whatsapp'
import { xReadTool, xSearchTool, xUserTool, xWriteTool } from './x'
import { youtubeSearchTool } from './youtube/search'
import { sendSMSTool } from './twilio/sendSMS'
import { airtableReadTool, airtableWriteTool, airtableUpdateTool } from './airtable'
const logger = createLogger('Tools')
@@ -110,6 +111,7 @@ export const tools: Record<string, ToolConfig> = {
confluence_retrieve: confluenceRetrieveTool,
confluence_list: confluenceListTool,
confluence_update: confluenceUpdateTool,
twilio_send_sms: sendSMSTool,
dalle_generate: dalleTool,
airtable_read: airtableReadTool,
airtable_write: airtableWriteTool,
+3
View File
@@ -0,0 +1,3 @@
import { sendSMSTool } from './sendSMS'
export const twilioSendSMSTool = sendSMSTool
+115
View File
@@ -0,0 +1,115 @@
import { createLogger } from '@/lib/logs/console-logger'
import { ToolConfig } from '../types'
import { TwilioSMSBlockOutput, TwilioSendSMSParams } from './types'
const logger = createLogger('Twilio Send SMS Tool')
export const sendSMSTool: ToolConfig<TwilioSendSMSParams, TwilioSMSBlockOutput> = {
id: 'twilio_send_sms',
name: 'Twilio Send SMS',
description: 'Send text messages to single or multiple recipients using the Twilio API.',
version: '1.0.0',
params: {
phoneNumbers: {
type: 'string',
required: true,
description: 'Phone numbers to send the message to, separated by newlines'
},
message: {
type: 'string',
required: true,
description: 'Message to send'
},
accountSid: {
type: 'string',
required: true,
description: 'Twilio Account SID',
requiredForToolCall: true
},
authToken: {
type: 'string',
required: true,
description: 'Twilio Auth Token',
requiredForToolCall: true
},
fromNumber: {
type: 'string',
required: true,
description: 'Twilio phone number to send the message from'
}
},
request: {
url: (params) => {
if (!params.accountSid) {
throw new Error('Twilio Account SID is required')
}
const url = `https://api.twilio.com/2010-04-01/Accounts/${params.accountSid}/Messages.json`;
return url
},
method: 'POST',
headers: (params) => {
if (!params.accountSid || !params.authToken) {
throw new Error('Twilio credentials are required')
}
// Use Buffer instead of btoa for Node.js compatibility
const authToken = Buffer.from(`${params.accountSid}:${params.authToken}`).toString('base64')
const headers = {
Authorization: `Basic ${authToken}`,
'Content-Type': 'application/x-www-form-urlencoded'
}
return headers
},
body: (params) => {
if (!params.phoneNumbers) {
throw new Error('Phone numbers are required but not provided')
}
if (!params.message) {
throw new Error('Message content is required but not provided')
}
if (!params.fromNumber) {
throw new Error('From number is required but not provided')
}
// Get first phone number if multiple are provided
const toNumber = params.phoneNumbers.split('\n')[0].trim()
// Create a URLSearchParams object and convert to string
const formData = new URLSearchParams()
formData.append('To', toNumber)
formData.append('From', params.fromNumber)
formData.append('Body', params.message)
const formDataString = formData.toString()
return { body: formDataString }
}
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
const errorMessage = data.error?.message || data.message || `Failed to send SMS (HTTP ${response.status})`
logger.error('Twilio API error:', data)
throw new Error(errorMessage)
}
logger.info('Twilio Response:', data)
logger.info('Twilio Response type:', typeof data)
return {
success: true,
output: {
success: true,
messageId: data.sid,
status: data.status
},
error: undefined
}
},
transformError: (error) => {
logger.error('Twilio tool error:', { error })
return `SMS sending failed: ${error.message || 'Unknown error occurred'}`
}
}
+18
View File
@@ -0,0 +1,18 @@
import { ToolResponse } from '../types'
export interface TwilioSendSMSParams {
phoneNumbers: string
message: string
accountSid: string
authToken: string
fromNumber: string
}
export interface TwilioSMSBlockOutput extends ToolResponse {
output: {
success: boolean
messageId?: string
status?: string
error?: string
}
}
+3 -3
View File
@@ -47,10 +47,10 @@ export function formatRequestParams(tool: ToolConfig, params: Record<string, any
const hasBody = method !== 'GET' && method !== 'HEAD' && !!tool.request.body
const bodyResult = tool.request.body ? tool.request.body(params) : undefined
// Special handling for NDJSON content type
const isNDJSON = headers['Content-Type'] === 'application/x-ndjson'
// Special handling for NDJSON content type or 'application/x-www-form-urlencoded'
const isPreformattedContent = headers['Content-Type'] === 'application/x-ndjson' || headers['Content-Type'] === 'application/x-www-form-urlencoded'
const body = hasBody
? isNDJSON && bodyResult
? isPreformattedContent && bodyResult
? bodyResult.body
: JSON.stringify(bodyResult)
: undefined