From a678f920d9ea1575f8d0ab9d65b8cbcc90fd8407 Mon Sep 17 00:00:00 2001 From: Vignesh Aigal Date: Sat, 30 Dec 2023 23:01:22 -0800 Subject: [PATCH 1/3] Add twilio webhook update url --- llmstack/apps/app_types.py | 1 + llmstack/apps/models.py | 7 ++- llmstack/apps/types/twilio_sms.py | 86 +++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 llmstack/apps/types/twilio_sms.py diff --git a/llmstack/apps/app_types.py b/llmstack/apps/app_types.py index b7b7bbbb7f8..223d929989d 100644 --- a/llmstack/apps/app_types.py +++ b/llmstack/apps/app_types.py @@ -7,6 +7,7 @@ from .types.web import WebApp from .types.discord import DiscordApp from .types.slack import SlackApp +from .types.twilio_sms import TwilioSmsApp class AppTypeFactory: diff --git a/llmstack/apps/models.py b/llmstack/apps/models.py index a9ad57c21ba..5ae0bf6c981 100644 --- a/llmstack/apps/models.py +++ b/llmstack/apps/models.py @@ -16,6 +16,7 @@ logger = logging.getLogger(__name__) + class AppVisibility(models.IntegerChoices): PRIVATE = 0, 'Private' # only the owner of the app and listed emails can access the app # only members of the organization can access the app @@ -237,7 +238,7 @@ def slack_config(self): def discord_config(self): profile = Profile.objects.get(user=self.owner) return DiscordIntegrationConfig().from_dict(self.discord_integration_config, profile.decrypt_value) if self.discord_integration_config else None - + @property def twilio_config(self): profile = Profile.objects.get(user=self.owner) @@ -408,3 +409,7 @@ def update_app_pre_save(sender, instance, **kwargs): instance.type, 'slack', ) instance = slack_app_type_handler_cls.pre_save(instance) + + twilio_sms_type_handler_cls = AppTypeFactory.get_app_type_handler( + instance.type, 'twilio_sms') + instance = twilio_sms_type_handler_cls.pre_save(instance) diff --git a/llmstack/apps/types/twilio_sms.py b/llmstack/apps/types/twilio_sms.py new file mode 100644 index 00000000000..fdfeab282f7 --- /dev/null +++ b/llmstack/apps/types/twilio_sms.py @@ -0,0 +1,86 @@ +import logging +from typing import List + +from pydantic import Field +import requests +from llmstack.apps.models import App +from llmstack.apps.types.app_type_interface import AppTypeInterface, BaseSchema + +logger = logging.getLogger(__name__) + + +class TwilioSmsAppConfigSchema(BaseSchema): + account_sid: str = Field( + title='Account SID', description="Account SID of the Twilio account. Your account's SID can be found in the console.", required=True, + ) + auth_token: str = Field( + title='Auth Token', widget='password', + description="Auth token of the Twilio account. Your account's auth token can be found in the console.", required=True, + ) + phone_numbers: List[str] = Field( + title='Phone Numbers', description='Phone numbers to send SMS messages from.', required=True, + ) + + +class TwilioSmsApp(AppTypeInterface[TwilioSmsAppConfigSchema]): + @staticmethod + def slug() -> str: + return 'twilio_sms' + + @staticmethod + def name() -> str: + return 'Twilio SMS App' + + @staticmethod + def description() -> str: + return 'Send SMS messages from Twilio.' + + @classmethod + def pre_save(self, app: App): + if app.is_published and app.twilio_config: + config = app.twilio_config + phone_numbers = config.get('phone_numbers', []) + + account_sid = config.get('account_sid', None) + auth_token = config.get('auth_token', None) + auto_create_sms_webhook = config.get( + 'auto_create_sms_webhook', False) + + if not phone_numbers: + raise Exception( + 'You must provide at least one phone number to send SMS messages from.') + if not account_sid or not auth_token: + raise Exception( + 'You must provide an account SID and auth token to send SMS messages from.') + + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + } + auth = (account_sid, auth_token) + for phone_number in phone_numbers: + ph_no = phone_number.strip().replace('+', '').replace('-', '') + response = requests.get( + f'https://api.twilio.com/2010-04-01/Accounts/{account_sid}/IncomingPhoneNumbers.json?PhoneNumber=%2B{ph_no}', + headers=headers, auth=auth) + if response.status_code != 200: + raise Exception( + f'Invalid phone number {phone_number}. Please provide a valid phone number that you own.') + twilio_phone_number_resource = response.json()[ + 'incoming_phone_numbers'][0] + sms_url = twilio_phone_number_resource['sms_url'] + # Create SMS webhook if it doesn't exist + if auto_create_sms_webhook and (not sms_url or sms_url != f'https://trypromptly.com/api/apps/{app.uuid}/twiliosms/run'): + # Update twilio phone number resource with voice webhook + response = requests.post( + f'https://api.twilio.com/2010-04-01/Accounts/{account_sid}/IncomingPhoneNumbers/{twilio_phone_number_resource["sid"]}.json', + headers=headers, auth=auth, data={ + 'SmsUrl': f'https://trypromptly.com/api/apps/{app.uuid}/twiliosms/run', + }) + if response.status_code != 200: + raise Exception( + f'Failed to update SMS webhook for phone number {phone_number}. Error: {response.text}') + logger.info( + f'{response.text} for phone number {phone_number}') + + return app From 0cc7bd33356194b97d264a60cd07b3b579562ea6 Mon Sep 17 00:00:00 2001 From: Vignesh Aigal Date: Mon, 1 Jan 2024 11:04:49 -0800 Subject: [PATCH 2/3] Update UI form to provide toggle option --- llmstack/apps/apis.py | 2 +- llmstack/apps/integration_configs.py | 5 +++-- .../client/src/components/apps/AppTwilioConfigEditor.jsx | 6 ++++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/llmstack/apps/apis.py b/llmstack/apps/apis.py index 2e15e0b5e43..984a1e160e2 100644 --- a/llmstack/apps/apis.py +++ b/llmstack/apps/apis.py @@ -6,7 +6,7 @@ from django.core.validators import validate_email from django.db.models import Q from django.forms import ValidationError -from django.http import HttpResponse, StreamingHttpResponse +from django.http import StreamingHttpResponse from django.shortcuts import get_object_or_404 from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_page diff --git a/llmstack/apps/integration_configs.py b/llmstack/apps/integration_configs.py index aa5ae7dab8f..3de93893e22 100644 --- a/llmstack/apps/integration_configs.py +++ b/llmstack/apps/integration_configs.py @@ -64,11 +64,12 @@ class DiscordIntegrationConfig(AppIntegrationConfig): bot_token: str = '' public_key: str = '' slash_command_id: Optional[str] = None - + + class TwilioIntegrationConfig(AppIntegrationConfig): config_type = 'twilio' is_encrypted = True account_sid: str = '' auth_token: str = '' phone_numbers: list = [] - + auto_create_sms_webhook = False diff --git a/llmstack/client/src/components/apps/AppTwilioConfigEditor.jsx b/llmstack/client/src/components/apps/AppTwilioConfigEditor.jsx index ef07c91477c..e00443c9244 100644 --- a/llmstack/client/src/components/apps/AppTwilioConfigEditor.jsx +++ b/llmstack/client/src/components/apps/AppTwilioConfigEditor.jsx @@ -30,6 +30,12 @@ const twilioConfigSchema = { type: "string", }, }, + auto_create_sms_webhook: { + type: "boolean", + title: "Create Twilio SMS Webhook", + description: + "Update Twilio SMS Webhook to point to send message to application", + }, }, }; From e6581ee04af1e29df48301dea64f2629cd88e1e8 Mon Sep 17 00:00:00 2001 From: Vignesh Aigal Date: Mon, 1 Jan 2024 11:44:10 -0800 Subject: [PATCH 3/3] Update --- llmstack/apps/types/twilio_sms.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/llmstack/apps/types/twilio_sms.py b/llmstack/apps/types/twilio_sms.py index fdfeab282f7..af91f7a9168 100644 --- a/llmstack/apps/types/twilio_sms.py +++ b/llmstack/apps/types/twilio_sms.py @@ -20,6 +20,9 @@ class TwilioSmsAppConfigSchema(BaseSchema): phone_numbers: List[str] = Field( title='Phone Numbers', description='Phone numbers to send SMS messages from.', required=True, ) + auto_create_sms_webhook: bool = Field(default=False, title='Auto Create SMS Webhook', + description='Automatically create an SMS webhook for the phone numbers.', required=False, + ) class TwilioSmsApp(AppTypeInterface[TwilioSmsAppConfigSchema]): @@ -80,7 +83,5 @@ def pre_save(self, app: App): if response.status_code != 200: raise Exception( f'Failed to update SMS webhook for phone number {phone_number}. Error: {response.text}') - logger.info( - f'{response.text} for phone number {phone_number}') return app