kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: handle message chunking in Twilio service
rodzic
0bfce67a7b
commit
5b510116e4
|
@ -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
|
||||
|
|
33
src/utils.ts
33
src/utils.ts
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
Ładowanie…
Reference in New Issue