feat: handle message chunking in Twilio service

old-agentic-v1^2
Philipp Burckhardt 2023-06-14 23:00:02 -04:00 zatwierdzone przez Travis Fischer
rodzic 0bfce67a7b
commit 5b510116e4
3 zmienionych plików z 88 dodań i 2 usunięć

Wyświetl plik

@ -1,7 +1,7 @@
import defaultKy from 'ky'
import { DEFAULT_BOT_NAME } from '@/constants'
import { sleep } from '@/utils'
import { chunkString, sleep } from '@/utils'
export const TWILIO_CONVERSATION_API_BASE_URL =
'https://conversations.twilio.com/v1'
@ -9,6 +9,14 @@ export const TWILIO_CONVERSATION_API_BASE_URL =
export const DEFAULT_TWILIO_TIMEOUT_MS = 120_000
export const DEFAULT_TWILIO_INTERVAL_MS = 5_000
/**
* Twilio recommends keeping SMS messages to a length of 320 characters or less, so we'll use that as the maximum.
*
* @see {@link https://support.twilio.com/hc/en-us/articles/360033806753-Maximum-Message-Length-with-Twilio-Programmable-Messaging}
*/
const TWILIO_SMS_LENGTH_SOFT_LIMIT = 320
const TWILIO_SMS_LENGTH_HARD_LIMIT = 1600
export interface TwilioConversation {
unique_name?: string
date_updated: Date
@ -229,6 +237,23 @@ export class TwilioConversationClient {
.json<TwilioConversationParticipant>()
}
/**
* Chunks a long text message into smaller parts and sends them as separate messages.
*/
async sendTextWithChunking({
conversationSid,
text
}: {
conversationSid: string
text: string
maxChunkLength?: number
}) {
const chunks = chunkString(text, TWILIO_SMS_LENGTH_SOFT_LIMIT)
return Promise.all(
chunks.map((chunk) => this.sendMessage({ conversationSid, text: chunk }))
)
}
/**
* Posts a message to a conversation.
*/
@ -239,6 +264,11 @@ export class TwilioConversationClient {
conversationSid: string
text: string
}) {
// Truncate the text if it exceeds the hard limit and add an ellipsis:
if (text.length > TWILIO_SMS_LENGTH_HARD_LIMIT) {
text = text.substring(0, TWILIO_SMS_LENGTH_HARD_LIMIT - 3) + '...'
}
const params = new URLSearchParams()
params.set('Body', text)
params.set('Author', this.botName)
@ -291,7 +321,7 @@ export class TwilioConversationClient {
const { sid: conversationSid } = await this.createConversation(name)
await this.addParticipant({ conversationSid, recipientPhoneNumber })
await this.sendMessage({ conversationSid, text })
await this.sendTextWithChunking({ conversationSid, text })
const start = Date.now()
let nUserMessages = 0

Wyświetl plik

@ -21,3 +21,36 @@ const taskNameRegex = /^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$/
export function isValidTaskIdentifier(id: string): boolean {
return !!id && taskNameRegex.test(id)
}
/**
* Chunk a string into an array of strings of a given length
*
* @param text - string to chunk
* @param length - maximum length of each chunk
* @returns array of strings
*/
export const chunkString = (text: string, length: number) => {
const words = text.split(' ')
const chunks: string[] = []
let chunk = ''
for (const word of words) {
if (word.length > length) {
// Truncate the word if it's too long and indicate that it was truncated:
chunks.push(word.substring(0, length - 3) + '...')
}
if ((chunk + word).length > length) {
chunks.push(chunk.trim())
chunk = word
} else {
chunk += ' ' + word
}
}
if (chunk) {
chunks.push(chunk.trim())
}
return chunks
}

Wyświetl plik

@ -58,6 +58,29 @@ test.serial('TwilioConversationClient.sendMessage', async (t) => {
await client.deleteConversation(conversationSid)
})
test.serial('TwilioConversationClient.sendTextWithChunking', async (t) => {
if (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN) {
return t.pass()
}
const client = new TwilioConversationClient()
const { sid: conversationSid } = await client.createConversation(
'send-message-test'
)
const text =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
const messages = await client.sendTextWithChunking({ conversationSid, text })
// Text should be sent in two messages:
t.true(text.startsWith(messages[0].body))
t.true(text.endsWith(messages[1].body))
await client.deleteConversation(conversationSid)
})
test.serial('TwilioConversationClient.fetchMessages', async (t) => {
if (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN) {
return t.pass()