diff --git a/packages/components/credentials/TelnyxApi.credential.ts b/packages/components/credentials/TelnyxApi.credential.ts new file mode 100644 index 00000000000..6bdd70715f0 --- /dev/null +++ b/packages/components/credentials/TelnyxApi.credential.ts @@ -0,0 +1,23 @@ +import { INodeParams, INodeCredential } from '../src/Interface' + +class TelnyxApi implements INodeCredential { + label: string + name: string + version: number + inputs: INodeParams[] + + constructor() { + this.label = 'Telnyx API' + this.name = 'telnyxApi' + this.version = 1.0 + this.inputs = [ + { + label: 'API Key', + name: 'apiKey', + type: 'password' + } + ] + } +} + +module.exports = { credClass: TelnyxApi } diff --git a/packages/components/nodes/chatmodels/ChatTelnyx/ChatTelnyx.ts b/packages/components/nodes/chatmodels/ChatTelnyx/ChatTelnyx.ts new file mode 100644 index 00000000000..f7757c1ffde --- /dev/null +++ b/packages/components/nodes/chatmodels/ChatTelnyx/ChatTelnyx.ts @@ -0,0 +1,136 @@ +import { ChatOpenAI, ChatOpenAIFields } from '@langchain/openai' +import { BaseCache } from '@langchain/core/caches' +import { ICommonObject, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../src/Interface' +import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { secureFetch } from '../../../src/httpSecurity' + +const TELNYX_OPENAI_BASE = 'https://api.telnyx.com/v2/ai/openai' +const TELNYX_CHAT_MODELS_URL = 'https://api.telnyx.com/v2/ai/openai/models' + +const fetchTelnyxModels = async (apiKey: string) => { + const response = await secureFetch(TELNYX_CHAT_MODELS_URL, { + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + } + }) + + if (!response.ok) { + throw new Error(`Failed to fetch Telnyx models: ${response.status} ${response.statusText}`) + } + + const json = await response.json() + return json.data || [] +} + +class ChatTelnyx_ChatModels implements INode { + label: string + name: string + version: number + type: string + icon: string + category: string + description: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + + constructor() { + this.label = 'Telnyx Chat' + this.name = 'chatTelnyx' + this.version = 1.1 + this.type = 'ChatTelnyx' + this.icon = 'telnyx.png' + this.category = 'Chat Models' + this.description = 'Use Telnyx OpenAI-compatible chat completions as a native Flowise chat model' + this.baseClasses = [this.type, ...getBaseClasses(ChatOpenAI)] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['telnyxApi'], + refresh: true + } + this.inputs = [ + { label: 'Cache', name: 'cache', type: 'BaseCache', optional: true }, + { label: 'Model Name', name: 'modelName', type: 'asyncOptions', loadMethod: 'listModels', default: 'openai/gpt-4o', refresh: true }, + { label: 'Temperature', name: 'temperature', type: 'number', step: 0.1, default: 0.9, optional: true }, + { label: 'Streaming', name: 'streaming', type: 'boolean', default: true, optional: true, additionalParams: true }, + { label: 'Max Tokens', name: 'maxTokens', type: 'number', step: 1, optional: true, additionalParams: true }, + { label: 'Top Probability', name: 'topP', type: 'number', step: 0.1, optional: true, additionalParams: true }, + { label: 'Frequency Penalty', name: 'frequencyPenalty', type: 'number', step: 0.1, optional: true, additionalParams: true }, + { label: 'Presence Penalty', name: 'presencePenalty', type: 'number', step: 0.1, optional: true, additionalParams: true }, + { label: 'Timeout', name: 'timeout', type: 'number', step: 1, optional: true, additionalParams: true } + ] + } + + //@ts-ignore + loadMethods = { + async listModels(nodeData: INodeData, options: ICommonObject): Promise { + const credentialId = nodeData.credential || nodeData.inputs?.credentialId + if (!credentialId) { + return [{ label: 'Select a Telnyx API credential to load models', name: 'openai/gpt-4o' }] + } + + try { + const credentialData = await getCredentialData(credentialId as string, options) + const apiKey = getCredentialParam('apiKey', credentialData, nodeData) + const models = await fetchTelnyxModels(apiKey) + + return models + .map((model: any) => ({ + label: model.id, + name: model.id, + description: [model.task, model.context_length ? `context ${model.context_length}` : '', model.tier || ''] + .filter(Boolean) + .join(' • ') + })) + } catch (error) { + console.warn('Falling back to static Telnyx chat model list:', error) + return [{ label: 'openai/gpt-4o', name: 'openai/gpt-4o' }] + } + } + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const temperature = nodeData.inputs?.temperature as string + const modelName = nodeData.inputs?.modelName as string + const maxTokens = nodeData.inputs?.maxTokens as string + const topP = nodeData.inputs?.topP as string + const frequencyPenalty = nodeData.inputs?.frequencyPenalty as string + const presencePenalty = nodeData.inputs?.presencePenalty as string + const timeout = nodeData.inputs?.timeout as string + const streaming = nodeData.inputs?.streaming as boolean + const cache = nodeData.inputs?.cache as BaseCache + + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const apiKey = getCredentialParam('apiKey', credentialData, nodeData) + + const parsedTemperature = temperature ? parseFloat(temperature) : 0.9 + if (Number.isNaN(parsedTemperature)) { + throw new Error('Temperature must be a valid number') + } + + const obj: ChatOpenAIFields = { + temperature: parsedTemperature, + modelName, + openAIApiKey: apiKey, + apiKey, + streaming: streaming ?? true, + configuration: { + baseURL: TELNYX_OPENAI_BASE + } + } + + if (maxTokens) obj.maxTokens = parseInt(maxTokens, 10) + if (topP) obj.topP = parseFloat(topP) + if (frequencyPenalty) obj.frequencyPenalty = parseFloat(frequencyPenalty) + if (presencePenalty) obj.presencePenalty = parseFloat(presencePenalty) + if (timeout) obj.timeout = parseInt(timeout, 10) + if (cache) obj.cache = cache + + return new ChatOpenAI(obj) + } +} + +module.exports = { nodeClass: ChatTelnyx_ChatModels } diff --git a/packages/components/nodes/chatmodels/ChatTelnyx/README.md b/packages/components/nodes/chatmodels/ChatTelnyx/README.md new file mode 100644 index 00000000000..4ca10e66776 --- /dev/null +++ b/packages/components/nodes/chatmodels/ChatTelnyx/README.md @@ -0,0 +1,13 @@ +# Telnyx Chat Model + +Telnyx Chat Model integration for Flowise + +## 🌱 Env Variables + +| Variable | Description | Type | Default | +| --------------- | ----------------------------------------------------- | ------ | ------- | +| TELNYX_API_KEY | Default `credential.apiKey` for the Telnyx API | String | | + +## License + +Source code in this repository is made available under the [Apache License Version 2.0](https://github.com/FlowiseAI/Flowise/blob/master/LICENSE.md). diff --git a/packages/components/nodes/chatmodels/ChatTelnyx/telnyx.png b/packages/components/nodes/chatmodels/ChatTelnyx/telnyx.png new file mode 100644 index 00000000000..13ea5d395e5 Binary files /dev/null and b/packages/components/nodes/chatmodels/ChatTelnyx/telnyx.png differ diff --git a/packages/components/nodes/embeddings/TelnyxEmbedding/README.md b/packages/components/nodes/embeddings/TelnyxEmbedding/README.md new file mode 100644 index 00000000000..f4686a26ce8 --- /dev/null +++ b/packages/components/nodes/embeddings/TelnyxEmbedding/README.md @@ -0,0 +1,13 @@ +# Telnyx Embedding Model + +Telnyx Embedding Model integration for Flowise + +## 🌱 Env Variables + +| Variable | Description | Type | Default | +| --------------- | ----------------------------------------------------- | ------ | ------- | +| TELNYX_API_KEY | Default `credential.apiKey` for the Telnyx API | String | | + +## License + +Source code in this repository is made available under the [Apache License Version 2.0](https://github.com/FlowiseAI/Flowise/blob/master/LICENSE.md). diff --git a/packages/components/nodes/embeddings/TelnyxEmbedding/TelnyxEmbedding.ts b/packages/components/nodes/embeddings/TelnyxEmbedding/TelnyxEmbedding.ts new file mode 100644 index 00000000000..bb9502b4cf1 --- /dev/null +++ b/packages/components/nodes/embeddings/TelnyxEmbedding/TelnyxEmbedding.ts @@ -0,0 +1,120 @@ +import { ClientOptions, OpenAIEmbeddings, OpenAIEmbeddingsParams } from '@langchain/openai' +import { ICommonObject, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../src/Interface' +import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { secureFetch } from '../../../src/httpSecurity' + +const TELNYX_OPENAI_BASE = 'https://api.telnyx.com/v2/ai/openai' +const TELNYX_EMBEDDINGS_MODELS_URL = 'https://api.telnyx.com/v2/ai/embeddings/models' + +const fetchTelnyxModels = async (apiKey: string) => { + const response = await secureFetch(TELNYX_EMBEDDINGS_MODELS_URL, { + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + } + }) + + if (!response.ok) { + throw new Error(`Failed to fetch Telnyx models: ${response.status} ${response.statusText}`) + } + + const json = await response.json() + return json.data || [] +} + +class TelnyxEmbedding_Embeddings implements INode { + label: string + name: string + version: number + type: string + icon: string + category: string + description: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + + constructor() { + this.label = 'Telnyx Embeddings' + this.name = 'telnyxEmbeddings' + this.version = 1.1 + this.type = 'TelnyxEmbeddings' + this.icon = 'telnyx.png' + this.category = 'Embeddings' + this.description = 'Use Telnyx OpenAI-compatible embeddings as a native Flowise embeddings node' + this.baseClasses = [this.type, ...getBaseClasses(OpenAIEmbeddings)] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['telnyxApi'], + refresh: true + } + this.inputs = [ + { label: 'Model Name', name: 'modelName', type: 'asyncOptions', loadMethod: 'listModels', default: 'text-embedding-3-small', refresh: true }, + { label: 'Strip New Lines', name: 'stripNewLines', type: 'boolean', optional: true, additionalParams: true }, + { label: 'Batch Size', name: 'batchSize', type: 'number', optional: true, additionalParams: true }, + { label: 'Timeout', name: 'timeout', type: 'number', optional: true, additionalParams: true }, + { label: 'Dimensions', name: 'dimensions', type: 'number', optional: true, additionalParams: true }, + { label: 'Encoding Format', name: 'encodingFormat', type: 'options', options: [{ label: 'float', name: 'float' }, { label: 'base64', name: 'base64' }], optional: true, additionalParams: true } + ] + } + + //@ts-ignore + loadMethods = { + async listModels(nodeData: INodeData, options: ICommonObject): Promise { + const credentialId = nodeData.credential || nodeData.inputs?.credentialId + if (!credentialId) { + return [{ label: 'Select a Telnyx API credential to load models', name: 'text-embedding-3-small' }] + } + + try { + const credentialData = await getCredentialData(credentialId as string, options) + const apiKey = getCredentialParam('apiKey', credentialData, nodeData) + const models = await fetchTelnyxModels(apiKey) + + return models + .map((model: any) => ({ + label: model.id, + name: model.id, + description: [model.task, model.context_length ? `context ${model.context_length}` : '', model.tier || ''] + .filter(Boolean) + .join(' • ') + })) + } catch (error) { + console.warn('Falling back to static Telnyx embeddings model list:', error) + return [{ label: 'text-embedding-3-small', name: 'text-embedding-3-small' }] + } + } + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const stripNewLines = nodeData.inputs?.stripNewLines as boolean + const batchSize = nodeData.inputs?.batchSize as string + const timeout = nodeData.inputs?.timeout as string + const modelName = nodeData.inputs?.modelName as string + const dimensions = nodeData.inputs?.dimensions as string + const encodingFormat = nodeData.inputs?.encodingFormat as 'float' | 'base64' | undefined + + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const apiKey = getCredentialParam('apiKey', credentialData, nodeData) + + const obj: Partial & { openAIApiKey?: string; configuration?: ClientOptions } = { + openAIApiKey: apiKey, + modelName, + configuration: { + baseURL: TELNYX_OPENAI_BASE + } + } + + if (stripNewLines) obj.stripNewLines = stripNewLines + if (batchSize) obj.batchSize = parseInt(batchSize, 10) + if (timeout) obj.timeout = parseInt(timeout, 10) + if (dimensions) obj.dimensions = parseInt(dimensions, 10) + if (encodingFormat) obj.encodingFormat = encodingFormat + + return new OpenAIEmbeddings(obj) + } +} + +module.exports = { nodeClass: TelnyxEmbedding_Embeddings } diff --git a/packages/components/nodes/embeddings/TelnyxEmbedding/telnyx.png b/packages/components/nodes/embeddings/TelnyxEmbedding/telnyx.png new file mode 100644 index 00000000000..13ea5d395e5 Binary files /dev/null and b/packages/components/nodes/embeddings/TelnyxEmbedding/telnyx.png differ diff --git a/packages/components/nodes/tools/TelnyxMessaging/TelnyxMessaging.ts b/packages/components/nodes/tools/TelnyxMessaging/TelnyxMessaging.ts new file mode 100644 index 00000000000..2c6431bdd19 --- /dev/null +++ b/packages/components/nodes/tools/TelnyxMessaging/TelnyxMessaging.ts @@ -0,0 +1,122 @@ +import { DynamicStructuredTool } from '../OpenAPIToolkit/core' +import { z } from 'zod/v3' +import { secureFetch } from '../../../src/httpSecurity' +import { getBaseClasses, getCredentialData, getCredentialParam, handleErrorMessage } from '../../../src/utils' +import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' + +class TelnyxMessagingTool extends DynamicStructuredTool { + constructor(apiKey: string, defaultFrom?: string, defaultMessagingProfileId?: string) { + super({ + name: 'telnyx_send_sms', + description: 'Send an SMS message through Telnyx. Use this tool when you need to send a text message to a phone number in E.164 format.', + schema: z.object({ + to: z.string().describe('Destination phone number in E.164 format'), + text: z.string().describe('Text message body to send to the destination number'), + from: z.string().optional().describe('Sender phone number in E.164 format'), + messagingProfileId: z.string().optional().describe('Telnyx Messaging Profile ID to use for sending the message') + }), + baseUrl: '', + method: 'POST', + headers: {} + }) + this.apiKey = apiKey + this.defaultFrom = defaultFrom + this.defaultMessagingProfileId = defaultMessagingProfileId + } + + apiKey: string + defaultFrom?: string + defaultMessagingProfileId?: string + + async _call(arg: any): Promise { + const from = arg.from || this.defaultFrom + const messagingProfileId = arg.messagingProfileId || this.defaultMessagingProfileId + + if (!from && !messagingProfileId) { + throw new Error('Telnyx Messaging requires either a From number or a Messaging Profile ID') + } + + const body: Record = { + to: arg.to, + text: arg.text + } + + if (from) body.from = from + if (messagingProfileId) body.messaging_profile_id = messagingProfileId + + try { + const res = await secureFetch('https://api.telnyx.com/v2/messages', { + method: 'POST', + headers: { + Authorization: `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(body) + }) + + const text = await res.text() + if (!res.ok) { + throw new Error(`HTTP ${res.status}: ${text}`) + } + return text + } catch (error) { + throw new Error(handleErrorMessage(error)) + } + } +} + +class TelnyxMessaging_Tools implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + + constructor() { + this.label = 'Telnyx Messaging' + this.name = 'telnyxMessaging' + this.version = 1.0 + this.type = 'TelnyxMessaging' + this.icon = 'telnyx.png' + this.category = 'Tools' + this.description = 'Send outbound SMS messages through the Telnyx Messaging API.' + this.baseClasses = [this.type, 'Tool', ...getBaseClasses(TelnyxMessagingTool)] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['telnyxApi'] + } + this.inputs = [ + { + label: 'Default From Number', + name: 'from', + type: 'string', + description: 'Optional default sender number in E.164 format. Either this value or a Messaging Profile ID is required to send a message.', + optional: true + }, + { + label: 'Default Messaging Profile ID', + name: 'messagingProfileId', + type: 'string', + description: 'Optional default Telnyx Messaging Profile ID. Use this if you prefer profile-based sending instead of providing a from number.', + optional: true + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const apiKey = getCredentialParam('apiKey', credentialData, nodeData) + const from = nodeData.inputs?.from as string | undefined + const messagingProfileId = nodeData.inputs?.messagingProfileId as string | undefined + return new TelnyxMessagingTool(apiKey, from, messagingProfileId) + } +} + +module.exports = { nodeClass: TelnyxMessaging_Tools } diff --git a/packages/components/nodes/tools/TelnyxMessaging/telnyx.png b/packages/components/nodes/tools/TelnyxMessaging/telnyx.png new file mode 100644 index 00000000000..13ea5d395e5 Binary files /dev/null and b/packages/components/nodes/tools/TelnyxMessaging/telnyx.png differ diff --git a/packages/components/nodes/tools/TelnyxNumberLookup/TelnyxNumberLookup.ts b/packages/components/nodes/tools/TelnyxNumberLookup/TelnyxNumberLookup.ts new file mode 100644 index 00000000000..37a9fcf5674 --- /dev/null +++ b/packages/components/nodes/tools/TelnyxNumberLookup/TelnyxNumberLookup.ts @@ -0,0 +1,83 @@ +import { DynamicStructuredTool } from '../OpenAPIToolkit/core' +import { z } from 'zod/v3' +import { secureFetch } from '../../../src/httpSecurity' +import { getBaseClasses, getCredentialData, getCredentialParam, handleErrorMessage } from '../../../src/utils' +import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' + +class TelnyxNumberLookupTool extends DynamicStructuredTool { + constructor(apiKey: string) { + super({ + name: 'telnyx_number_lookup', + description: 'Look up a phone number with Telnyx. Use this to validate a number, inspect carrier details, and understand the line type before sending or calling.', + schema: z.object({ + phoneNumber: z.string().describe('Phone number to lookup in E.164 format') + }), + baseUrl: '', + method: 'GET', + headers: {} + }) + this.apiKey = apiKey + } + + apiKey: string + + async _call(arg: any): Promise { + try { + const url = `https://api.telnyx.com/v2/number_lookup/${encodeURIComponent(arg.phoneNumber)}` + const res = await secureFetch(url, { + method: 'GET', + headers: { + Authorization: `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json' + } + }) + + const text = await res.text() + if (!res.ok) { + throw new Error(`HTTP ${res.status}: ${text}`) + } + return text + } catch (error) { + throw new Error(handleErrorMessage(error)) + } + } +} + +class TelnyxNumberLookup_Tools implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + + constructor() { + this.label = 'Telnyx Number Lookup' + this.name = 'telnyxNumberLookup' + this.version = 1.0 + this.type = 'TelnyxNumberLookup' + this.icon = 'telnyx.png' + this.category = 'Tools' + this.description = 'Validate a phone number and fetch carrier and line type data through the Telnyx Number Lookup API.' + this.baseClasses = [this.type, 'Tool', ...getBaseClasses(TelnyxNumberLookupTool)] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['telnyxApi'] + } + this.inputs = [] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const apiKey = getCredentialParam('apiKey', credentialData, nodeData) + return new TelnyxNumberLookupTool(apiKey) + } +} + +module.exports = { nodeClass: TelnyxNumberLookup_Tools } diff --git a/packages/components/nodes/tools/TelnyxNumberLookup/telnyx.png b/packages/components/nodes/tools/TelnyxNumberLookup/telnyx.png new file mode 100644 index 00000000000..13ea5d395e5 Binary files /dev/null and b/packages/components/nodes/tools/TelnyxNumberLookup/telnyx.png differ diff --git a/packages/components/nodes/tools/TelnyxVerifyCheck/TelnyxVerifyCheck.ts b/packages/components/nodes/tools/TelnyxVerifyCheck/TelnyxVerifyCheck.ts new file mode 100644 index 00000000000..abb1524e6b9 --- /dev/null +++ b/packages/components/nodes/tools/TelnyxVerifyCheck/TelnyxVerifyCheck.ts @@ -0,0 +1,106 @@ +import { DynamicStructuredTool } from '../OpenAPIToolkit/core' +import { z } from 'zod/v3' +import { secureFetch } from '../../../src/httpSecurity' +import { getBaseClasses, getCredentialData, getCredentialParam, handleErrorMessage } from '../../../src/utils' +import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' + +class TelnyxVerifyCheckTool extends DynamicStructuredTool { + constructor(apiKey: string, defaultVerifyProfileId?: string) { + super({ + name: 'telnyx_verify_check', + description: 'Check a verification code with Telnyx Verify. Use this to validate an OTP submitted by a user for a phone number.', + schema: z.object({ + phoneNumber: z.string().describe('Phone number in E.164 format'), + code: z.string().describe('Verification code submitted by the user'), + verifyProfileId: z.string().optional().describe('Telnyx Verify Profile ID to use when checking the OTP code') + }), + baseUrl: '', + method: 'POST', + headers: {} + }) + this.apiKey = apiKey + this.defaultVerifyProfileId = defaultVerifyProfileId + } + + apiKey: string + defaultVerifyProfileId?: string + + async _call(arg: any): Promise { + const verifyProfileId = arg.verifyProfileId || this.defaultVerifyProfileId + if (!verifyProfileId) { + throw new Error('Telnyx Verify Check requires a Verify Profile ID') + } + + const body = { + verify_profile_id: verifyProfileId, + code: arg.code + } + + try { + const res = await secureFetch(`https://api.telnyx.com/v2/verifications/by_phone_number/${encodeURIComponent(arg.phoneNumber)}/actions/verify`, { + method: 'POST', + headers: { + Authorization: `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(body) + }) + + const text = await res.text() + if (!res.ok) { + throw new Error(`HTTP ${res.status}: ${text}`) + } + return text + } catch (error) { + throw new Error(handleErrorMessage(error)) + } + } +} + +class TelnyxVerifyCheck_Tools implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + + constructor() { + this.label = 'Telnyx Verify Check' + this.name = 'telnyxVerifyCheck' + this.version = 1.0 + this.type = 'TelnyxVerifyCheck' + this.icon = 'telnyx.png' + this.category = 'Tools' + this.description = 'Validate OTP codes through the Telnyx Verify API after the user submits a verification code.' + this.baseClasses = [this.type, 'Tool', ...getBaseClasses(TelnyxVerifyCheckTool)] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['telnyxApi'] + } + this.inputs = [ + { + label: 'Default Verify Profile ID', + name: 'verifyProfileId', + type: 'string', + description: 'Optional default Telnyx Verify Profile ID used when validating OTP codes.', + optional: true + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const apiKey = getCredentialParam('apiKey', credentialData, nodeData) + const verifyProfileId = nodeData.inputs?.verifyProfileId as string | undefined + return new TelnyxVerifyCheckTool(apiKey, verifyProfileId) + } +} + +module.exports = { nodeClass: TelnyxVerifyCheck_Tools } diff --git a/packages/components/nodes/tools/TelnyxVerifyCheck/telnyx.png b/packages/components/nodes/tools/TelnyxVerifyCheck/telnyx.png new file mode 100644 index 00000000000..13ea5d395e5 Binary files /dev/null and b/packages/components/nodes/tools/TelnyxVerifyCheck/telnyx.png differ diff --git a/packages/components/nodes/tools/TelnyxVerifySend/TelnyxVerifySend.ts b/packages/components/nodes/tools/TelnyxVerifySend/TelnyxVerifySend.ts new file mode 100644 index 00000000000..d9afc29f772 --- /dev/null +++ b/packages/components/nodes/tools/TelnyxVerifySend/TelnyxVerifySend.ts @@ -0,0 +1,107 @@ +import { DynamicStructuredTool } from '../OpenAPIToolkit/core' +import { z } from 'zod/v3' +import { secureFetch } from '../../../src/httpSecurity' +import { getBaseClasses, getCredentialData, getCredentialParam, handleErrorMessage } from '../../../src/utils' +import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' + +class TelnyxVerifySendTool extends DynamicStructuredTool { + constructor(apiKey: string, defaultVerifyProfileId?: string) { + super({ + name: 'telnyx_verify_send', + description: 'Send a verification code with Telnyx Verify. Use this to start an OTP verification flow for a phone number.', + schema: z.object({ + phoneNumber: z.string().describe('Destination phone number in E.164 format'), + verifyProfileId: z.string().optional().describe('Telnyx Verify Profile ID to use for this verification request'), + channel: z.enum(['sms', 'call']).optional().describe('Verification channel to use when delivering the OTP code') + }), + baseUrl: '', + method: 'POST', + headers: {} + }) + this.apiKey = apiKey + this.defaultVerifyProfileId = defaultVerifyProfileId + } + + apiKey: string + defaultVerifyProfileId?: string + + async _call(arg: any): Promise { + const verifyProfileId = arg.verifyProfileId || this.defaultVerifyProfileId + if (!verifyProfileId) { + throw new Error('Telnyx Verify Send requires a Verify Profile ID') + } + + const body = { + phone_number: arg.phoneNumber, + verify_profile_id: verifyProfileId, + channel: arg.channel || 'sms' + } + + try { + const res = await secureFetch('https://api.telnyx.com/v2/verifications', { + method: 'POST', + headers: { + Authorization: `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(body) + }) + + const text = await res.text() + if (!res.ok) { + throw new Error(`HTTP ${res.status}: ${text}`) + } + return text + } catch (error) { + throw new Error(handleErrorMessage(error)) + } + } +} + +class TelnyxVerifySend_Tools implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + + constructor() { + this.label = 'Telnyx Verify Send' + this.name = 'telnyxVerifySend' + this.version = 1.0 + this.type = 'TelnyxVerifySend' + this.icon = 'telnyx.png' + this.category = 'Tools' + this.description = 'Start an OTP verification flow through the Telnyx Verify API.' + this.baseClasses = [this.type, 'Tool', ...getBaseClasses(TelnyxVerifySendTool)] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['telnyxApi'] + } + this.inputs = [ + { + label: 'Default Verify Profile ID', + name: 'verifyProfileId', + type: 'string', + description: 'Optional default Telnyx Verify Profile ID used when sending OTP codes.', + optional: true + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const apiKey = getCredentialParam('apiKey', credentialData, nodeData) + const verifyProfileId = nodeData.inputs?.verifyProfileId as string | undefined + return new TelnyxVerifySendTool(apiKey, verifyProfileId) + } +} + +module.exports = { nodeClass: TelnyxVerifySend_Tools } diff --git a/packages/components/nodes/tools/TelnyxVerifySend/telnyx.png b/packages/components/nodes/tools/TelnyxVerifySend/telnyx.png new file mode 100644 index 00000000000..13ea5d395e5 Binary files /dev/null and b/packages/components/nodes/tools/TelnyxVerifySend/telnyx.png differ diff --git a/packages/components/src/speechToText.ts b/packages/components/src/speechToText.ts index e59368a5eae..b413ad0db58 100644 --- a/packages/components/src/speechToText.ts +++ b/packages/components/src/speechToText.ts @@ -11,7 +11,8 @@ const SpeechToTextType = { ASSEMBLYAI_TRANSCRIBE: 'assemblyAiTranscribe', LOCALAI_STT: 'localAISTT', AZURE_COGNITIVE: 'azureCognitive', - GROQ_WHISPER: 'groqWhisper' + GROQ_WHISPER: 'groqWhisper', + TELNYX_STT: 'telnyxStt' } export const convertSpeechToText = async (upload: IFileUpload, speechToTextConfig: ICommonObject, options: ICommonObject) => { @@ -81,7 +82,7 @@ export const convertSpeechToText = async (upload: IFileUpload, speechToTextConfi const formData = new FormData() const audioBlob = new Blob([new Uint8Array(audio_file)], { type: upload.type }) - formData.append('audio', audioBlob, upload.name) + formData.append('file', audioBlob, upload.name) const channelsStr = speechToTextConfig.channels || '0,1' const channels = channelsStr.split(',').map(Number) @@ -108,6 +109,29 @@ export const convertSpeechToText = async (upload: IFileUpload, speechToTextConfi throw error.response?.data || error } } + + case SpeechToTextType.TELNYX_STT: { + const formData = new FormData() + const audioBlob = new Blob([new Uint8Array(audio_file)], { type: upload.type || 'audio/mpeg' }) + formData.append('file', audioBlob, upload.name) + formData.append('model', speechToTextConfig?.model || 'openai/whisper-large-v3-turbo') + if (speechToTextConfig?.language) formData.append('language', speechToTextConfig.language) + if (speechToTextConfig?.prompt) formData.append('prompt', speechToTextConfig.prompt) + if (speechToTextConfig?.temperature) formData.append('temperature', speechToTextConfig.temperature) + + const response = await axios.post('https://api.telnyx.com/v2/ai/audio/transcriptions', formData, { + headers: { + Authorization: `Bearer ${credentialData.apiKey}`, + Accept: 'application/json' + } + }) + + const text = response?.data?.data?.text || response?.data?.text || response?.data?.transcript + if (text) { + return text + } + break + } case SpeechToTextType.GROQ_WHISPER: { const groqClient = new Groq({ apiKey: credentialData.groqApiKey diff --git a/packages/components/src/textToSpeech.ts b/packages/components/src/textToSpeech.ts index c4611806406..e3c7c99a5f6 100644 --- a/packages/components/src/textToSpeech.ts +++ b/packages/components/src/textToSpeech.ts @@ -3,11 +3,13 @@ import { getCredentialData } from './utils' import OpenAI from 'openai' import { ElevenLabsClient } from '@elevenlabs/elevenlabs-js' import { Readable } from 'node:stream' +import axios from 'axios' import type { ReadableStream } from 'node:stream/web' const TextToSpeechType = { OPENAI_TTS: 'openai', - ELEVEN_LABS_TTS: 'elevenlabs' + ELEVEN_LABS_TTS: 'elevenlabs', + TELNYX_TTS: 'telnyxTts' } export const convertTextToSpeechStream = async ( @@ -100,6 +102,39 @@ export const convertTextToSpeechStream = async ( }) break } + + + case TextToSpeechType.TELNYX_TTS: { + onStart((textToSpeechConfig.output_format || 'mp3') as string) + + const response = await axios.post( + 'https://api.telnyx.com/v2/text-to-speech/speech', + { + text, + voice: textToSpeechConfig.voice || 'Telnyx.NaturalHD.astra', + output_format: textToSpeechConfig.output_format || 'mp3', + sample_rate: textToSpeechConfig.sample_rate ? Number(textToSpeechConfig.sample_rate) : undefined, + language_code: textToSpeechConfig.language_code || undefined, + speed: textToSpeechConfig.speed ? Number(textToSpeechConfig.speed) : undefined + }, + { + headers: { + Authorization: `Bearer ${credentialData.apiKey}`, + 'Content-Type': 'application/json', + Accept: 'audio/mpeg' + }, + responseType: 'arraybuffer', + signal: abortController.signal + } + ) + + const buffer = Buffer.from(response.data) + const stream = Readable.from(buffer) + await processStreamWithRateLimit(stream, onChunk, onEnd, resolve, reject, 640, 20, abortController, () => { + streamDestroyed = true + }) + break + } } } else { reject(new Error('Text to speech is not selected. Please configure TTS in the chatflow.')) diff --git a/packages/ui/src/assets/images/telnyx.png b/packages/ui/src/assets/images/telnyx.png new file mode 100644 index 00000000000..13ea5d395e5 Binary files /dev/null and b/packages/ui/src/assets/images/telnyx.png differ diff --git a/packages/ui/src/ui-component/extended/SpeechToText.jsx b/packages/ui/src/ui-component/extended/SpeechToText.jsx index 2ca7fd95c28..cf38731dce2 100644 --- a/packages/ui/src/ui-component/extended/SpeechToText.jsx +++ b/packages/ui/src/ui-component/extended/SpeechToText.jsx @@ -20,6 +20,7 @@ import assemblyAIPng from '@/assets/images/assemblyai.png' import localAiPng from '@/assets/images/localai.png' import azureSvg from '@/assets/images/azure_openai.svg' import groqPng from '@/assets/images/groq.png' +import telnyxPng from '@/assets/images/telnyx.png' // store import useNotifier from '@/utils/useNotifier' @@ -34,7 +35,8 @@ const SpeechToTextType = { ASSEMBLYAI_TRANSCRIBE: 'assemblyAiTranscribe', LOCALAI_STT: 'localAISTT', AZURE_COGNITIVE: 'azureCognitive', - GROQ_WHISPER: 'groqWhisper' + GROQ_WHISPER: 'groqWhisper', + TELNYX_STT: 'telnyxStt' } // Weird quirk - the key must match the name property value. @@ -56,7 +58,7 @@ const speechToTextProviders = { name: 'language', type: 'string', description: - 'The language of the input audio. Supplying the input language in ISO-639-1 format will improve accuracy and latency.', + 'Optional language of the input audio. Supplying the language can improve recognition accuracy. Supplying the input language in ISO-639-1 format will improve accuracy and latency.', placeholder: 'en', optional: true }, @@ -115,7 +117,7 @@ const speechToTextProviders = { name: 'language', type: 'string', description: - 'The language of the input audio. Supplying the input language in ISO-639-1 format will improve accuracy and latency.', + 'Optional language of the input audio. Supplying the language can improve recognition accuracy. Supplying the input language in ISO-639-1 format will improve accuracy and latency.', placeholder: 'en', optional: true }, @@ -197,6 +199,52 @@ const speechToTextProviders = { } ] }, + [SpeechToTextType.TELNYX_STT]: { + label: 'Telnyx STT', + name: SpeechToTextType.TELNYX_STT, + icon: telnyxPng, + url: 'https://developers.telnyx.com/docs/voice/stt/models', + inputs: [ + { + label: 'Model', + name: 'model', + type: 'string', + description: 'Speech-to-text model to use. Defaults to openai/whisper-large-v3-turbo when left blank.', + placeholder: 'openai/whisper-large-v3-turbo', + optional: true + }, + { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['telnyxApi'] + }, + { + label: 'Language', + name: 'language', + type: 'string', + description: 'Optional language of the input audio. Supplying the language can improve recognition accuracy.', + placeholder: 'en', + optional: true + }, + { + label: 'Prompt', + name: 'prompt', + type: 'string', + rows: 4, + description: 'Optional prompt to guide the transcription style or continue a previous audio segment.', + optional: true + }, + { + label: 'Temperature', + name: 'temperature', + type: 'number', + step: 0.1, + description: 'Optional sampling temperature between 0 and 1. Lower values are more deterministic.', + optional: true + } + ] + }, [SpeechToTextType.GROQ_WHISPER]: { label: 'Groq Whisper', name: SpeechToTextType.GROQ_WHISPER, @@ -222,7 +270,7 @@ const speechToTextProviders = { name: 'language', type: 'string', description: - 'The language of the input audio. Supplying the input language in ISO-639-1 format will improve accuracy and latency.', + 'Optional language of the input audio. Supplying the language can improve recognition accuracy. Supplying the input language in ISO-639-1 format will improve accuracy and latency.', placeholder: 'en', optional: true }, diff --git a/packages/ui/src/ui-component/extended/TextToSpeech.jsx b/packages/ui/src/ui-component/extended/TextToSpeech.jsx index 8ce171615a4..34fabbdeefe 100644 --- a/packages/ui/src/ui-component/extended/TextToSpeech.jsx +++ b/packages/ui/src/ui-component/extended/TextToSpeech.jsx @@ -31,6 +31,7 @@ import { Dropdown } from '@/ui-component/dropdown/Dropdown' import AudioWaveform from '@/ui-component/extended/AudioWaveform' import openAISVG from '@/assets/images/openai.svg' import elevenLabsSVG from '@/assets/images/elevenlabs.svg' +import telnyxPng from '@/assets/images/telnyx.png' // store import useNotifier from '@/utils/useNotifier' @@ -41,7 +42,8 @@ import ttsApi from '@/api/tts' const TextToSpeechType = { OPENAI_TTS: 'openai', - ELEVEN_LABS_TTS: 'elevenlabs' + ELEVEN_LABS_TTS: 'elevenlabs', + TELNYX_TTS: 'telnyxTts' } // Weird quirk - the key must match the name property value. @@ -84,11 +86,64 @@ const textToSpeechProviders = { label: 'Voice', name: 'voice', type: 'voice_select', - description: 'The voice to use for text-to-speech', + description: 'Voice to use for text-to-speech synthesis', default: '21m00Tcm4TlvDq8ikWAM', optional: true } ] + }, + + [TextToSpeechType.TELNYX_TTS]: { + label: 'Telnyx TTS', + name: TextToSpeechType.TELNYX_TTS, + icon: telnyxPng, + url: 'https://developers.telnyx.com/docs/voice/programmable-voice/tts', + inputs: [ + { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['telnyxApi'] + }, + { + label: 'Voice', + name: 'voice', + type: 'voice_select', + description: 'Voice to use for text-to-speech synthesis', + default: 'Telnyx.NaturalHD.astra', + optional: true + }, + { + label: 'Output Format', + name: 'output_format', + type: 'string', + description: 'Audio output format returned by the Telnyx TTS API', + default: 'mp3', + optional: true + }, + { + label: 'Sample Rate', + name: 'sample_rate', + type: 'number', + description: 'Optional audio sample rate for the generated output.', + optional: true + }, + { + label: 'Language Code', + name: 'language_code', + type: 'string', + description: 'Optional language code to guide pronunciation or locale-specific speech output.', + optional: true + }, + { + label: 'Speed', + name: 'speed', + type: 'number', + step: 0.1, + description: 'Optional speech speed multiplier.', + optional: true + } + ] } }