Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Twilio Full #397

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
19 changes: 19 additions & 0 deletions bots/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,21 @@
"slack_create_personal_channels",
]
web_fields = ["web_allowed_origins", "web_config_extras"]
twilio_fields = [
"twilio_account_sid",
"twilio_auth_token",
"twilio_phone_number",
"twilio_phone_number_sid",
"twilio_default_to_gooey_asr",
"twilio_default_to_gooey_tts",
"twilio_voice",
"twilio_asr_language",
"twilio_initial_text",
"twilio_initial_audio_url",
"twilio_use_missed_call",
"twilio_waiting_audio_url",
"twilio_waiting_text",
]


class BotIntegrationAdminForm(forms.ModelForm):
Expand Down Expand Up @@ -133,12 +148,14 @@ class BotIntegrationAdmin(admin.ModelAdmin):
"slack_channel_name",
"slack_channel_hook_url",
"slack_access_token",
"twilio_phone_number",
]
list_display = [
"name",
"get_display_name",
"platform",
"wa_phone_number",
"twilio_phone_number",
"created_at",
"updated_at",
"billing_account_uid",
Expand Down Expand Up @@ -193,6 +210,7 @@ class BotIntegrationAdmin(admin.ModelAdmin):
*wa_fields,
*slack_fields,
*web_fields,
*twilio_fields,
]
},
),
Expand Down Expand Up @@ -489,6 +507,7 @@ class ConversationAdmin(admin.ModelAdmin):
"slack_user_name",
"slack_channel_id",
"slack_channel_name",
"twilio_phone_number",
] + [f"bot_integration__{field}" for field in BotIntegrationAdmin.search_fields]
actions = [export_to_csv, export_to_excel]

Expand Down
89 changes: 89 additions & 0 deletions bots/migrations/0077_botintegration_twilio_account_sid_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Generated by Django 4.2.7 on 2024-07-11 01:30

from django.db import migrations, models
import phonenumber_field.modelfields


class Migration(migrations.Migration):

dependencies = [
('bots', '0076_alter_workflowmetadata_default_image_and_more'),
]

operations = [
migrations.AddField(
model_name='botintegration',
name='twilio_account_sid',
field=models.TextField(blank=True, default='', help_text='Twilio account sid as found on twilio.com/console (mandatory)'),
),
migrations.AddField(
model_name='botintegration',
name='twilio_asr_language',
field=models.TextField(default='en-US', help_text='The language to use for Twilio ASR (https://www.twilio.com/docs/voice/twiml/gather#languagetags)'),
),
migrations.AddField(
model_name='botintegration',
name='twilio_auth_token',
field=models.TextField(blank=True, default='', help_text='Twilio auth token as found on twilio.com/console (mandatory)'),
),
migrations.AddField(
model_name='botintegration',
name='twilio_default_to_gooey_asr',
field=models.BooleanField(default=False, help_text="If true, the bot will use Gooey ASR for speech recognition instead of Twilio's when available on the attached run"),
),
migrations.AddField(
model_name='botintegration',
name='twilio_default_to_gooey_tts',
field=models.BooleanField(default=False, help_text="If true, the bot will use Gooey TTS for text to speech instead of Twilio's when available on the attached run"),
),
migrations.AddField(
model_name='botintegration',
name='twilio_initial_audio_url',
field=models.TextField(blank=True, default='', help_text='The initial audio url to play to the user when a call is started'),
),
migrations.AddField(
model_name='botintegration',
name='twilio_initial_text',
field=models.TextField(blank=True, default='', help_text='The initial text to send to the user when a call is started'),
),
migrations.AddField(
model_name='botintegration',
name='twilio_phone_number',
field=phonenumber_field.modelfields.PhoneNumberField(blank=True, default='', help_text='Twilio unformatted phone number as found on twilio.com/console/phone-numbers/incoming (mandatory)', max_length=128, region=None),
),
migrations.AddField(
model_name='botintegration',
name='twilio_phone_number_sid',
field=models.TextField(blank=True, default='', help_text='Twilio phone number sid as found on twilio.com/console/phone-numbers/incoming (mandatory)'),
),
migrations.AddField(
model_name='botintegration',
name='twilio_use_missed_call',
field=models.BooleanField(default=False, help_text="If true, the bot will reject incoming calls and call back the user instead so they don't get charged for the call"),
),
migrations.AddField(
model_name='botintegration',
name='twilio_voice',
field=models.TextField(default='woman', help_text="The voice to use for Twilio TTS ('man', 'woman', or Amazon Polly/Google Voices: https://www.twilio.com/docs/voice/twiml/say/text-speech#available-voices-and-languages)"),
),
migrations.AddField(
model_name='botintegration',
name='twilio_waiting_audio_url',
field=models.TextField(blank=True, default='', help_text='The audio url to play to the user while waiting for a response if using voice'),
),
migrations.AddField(
model_name='botintegration',
name='twilio_waiting_text',
field=models.TextField(blank=True, default='', help_text='The text to send to the user while waiting for a response if using sms'),
),
migrations.AddField(
model_name='conversation',
name='twilio_phone_number',
field=phonenumber_field.modelfields.PhoneNumberField(blank=True, default='', help_text="User's Twilio phone number (mandatory)", max_length=128, region=None),
),
migrations.AlterField(
model_name='botintegration',
name='platform',
field=models.IntegerField(choices=[(1, 'Facebook Messenger'), (2, 'Instagram'), (3, 'WhatsApp'), (4, 'Slack'), (5, 'Web'), (6, 'Twilio')], help_text='The platform that the bot is integrated with'),
),
]
94 changes: 93 additions & 1 deletion bots/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,14 @@ class Platform(models.IntegerChoices):
WHATSAPP = (3, "WhatsApp")
SLACK = (4, "Slack")
WEB = (5, "Web")
TWILIO = (6, "Twilio")

def get_icon(self):
match self:
case Platform.WEB:
return f'<i class="fa-regular fa-globe"></i>'
case Platform.TWILIO:
return f'<img src="https://storage.googleapis.com/dara-c1b52.appspot.com/daras_ai/media/73d11836-3988-11ef-9e06-02420a00011a/favicon-32x32.png" style="height: 1.2em; vertical-align: middle;">'
case _:
return f'<i class="fa-brands fa-{self.name.lower()}"></i>'

Expand Down Expand Up @@ -622,6 +625,67 @@ class BotIntegration(models.Model):
help_text="Extra configuration for the bot's web integration",
)

twilio_account_sid = models.TextField(
blank=True,
default="",
help_text="Twilio account sid as found on twilio.com/console (mandatory)",
)
twilio_auth_token = models.TextField(
blank=True,
default="",
help_text="Twilio auth token as found on twilio.com/console (mandatory)",
)
twilio_phone_number = PhoneNumberField(
blank=True,
default="",
help_text="Twilio unformatted phone number as found on twilio.com/console/phone-numbers/incoming (mandatory)",
)
twilio_phone_number_sid = models.TextField(
blank=True,
default="",
help_text="Twilio phone number sid as found on twilio.com/console/phone-numbers/incoming (mandatory)",
)
twilio_default_to_gooey_asr = models.BooleanField(
default=False,
help_text="If true, the bot will use Gooey ASR for speech recognition instead of Twilio's when available on the attached run",
)
twilio_default_to_gooey_tts = models.BooleanField(
default=False,
help_text="If true, the bot will use Gooey TTS for text to speech instead of Twilio's when available on the attached run",
)
twilio_voice = models.TextField(
default="woman",
help_text="The voice to use for Twilio TTS ('man', 'woman', or Amazon Polly/Google Voices: https://www.twilio.com/docs/voice/twiml/say/text-speech#available-voices-and-languages)",
)
twilio_initial_text = models.TextField(
default="",
blank=True,
help_text="The initial text to send to the user when a call is started",
)
twilio_initial_audio_url = models.TextField(
default="",
blank=True,
help_text="The initial audio url to play to the user when a call is started",
)
twilio_use_missed_call = models.BooleanField(
default=False,
help_text="If true, the bot will reject incoming calls and call back the user instead so they don't get charged for the call",
)
twilio_waiting_audio_url = models.TextField(
default="",
blank=True,
help_text="The audio url to play to the user while waiting for a response if using voice",
)
twilio_waiting_text = models.TextField(
default="",
blank=True,
help_text="The text to send to the user while waiting for a response if using sms",
)
twilio_asr_language = models.TextField(
default="en-US",
help_text="The language to use for Twilio ASR (https://www.twilio.com/docs/voice/twiml/gather#languagetags)",
)

streaming_enabled = models.BooleanField(
default=False,
help_text="If set, the bot will stream messages to the frontend (Slack & Web only)",
Expand Down Expand Up @@ -667,6 +731,7 @@ def get_display_name(self):
or " | #".join(
filter(None, [self.slack_team_name, self.slack_channel_name])
)
or (self.twilio_phone_number and self.twilio_phone_number.as_international)
or self.name
or (
self.platform == Platform.WEB
Expand Down Expand Up @@ -699,6 +764,21 @@ def get_web_widget_config(self, target="#gooey-embed") -> dict:
)
return config

def translate(self, text: str) -> str:
from daras_ai_v2.asr import run_google_translate, should_translate_lang

if text and should_translate_lang(self.user_language):
active_run = self.get_active_saved_run()
return run_google_translate(
[text],
self.user_language,
glossary_url=(
active_run.state.get("output_glossary") if active_run else None
),
)[0]
else:
return text


class BotIntegrationAnalysisRun(models.Model):
bot_integration = models.ForeignKey(
Expand Down Expand Up @@ -769,7 +849,12 @@ class ConversationQuerySet(models.QuerySet):
def get_unique_users(self) -> "ConversationQuerySet":
"""Get unique conversations"""
return self.distinct(
"fb_page_id", "ig_account_id", "wa_phone_number", "slack_user_id"
"fb_page_id",
"ig_account_id",
"wa_phone_number",
"slack_user_id",
"twilio_phone_number",
"web_user_id",
)

def to_df(self, tz=pytz.timezone(settings.TIME_ZONE)) -> "pd.DataFrame":
Expand Down Expand Up @@ -962,6 +1047,12 @@ class Conversation(models.Model):
help_text="Whether this is a personal slack channel between the bot and the user",
)

twilio_phone_number = PhoneNumberField(
blank=True,
default="",
help_text="User's Twilio phone number (mandatory)",
)

web_user_id = models.CharField(
max_length=512,
blank=True,
Expand Down Expand Up @@ -1007,6 +1098,7 @@ def get_display_name(self):
or self.fb_page_id
or self.slack_user_id
or self.web_user_id
or (self.twilio_phone_number and self.twilio_phone_number.as_international)
)

get_display_name.short_description = "User"
Expand Down
9 changes: 9 additions & 0 deletions bots/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
create_personal_channel,
SlackBot,
)
from daras_ai_v2.twilio_bot import create_voice_call, send_sms_message
from daras_ai_v2.vector_search import references_as_prompt
from gooeysite.bg_db_conn import get_celery_result_db_safe
from recipes.VideoBots import ReplyButton, messages_as_prompt
Expand Down Expand Up @@ -153,6 +154,7 @@ def send_broadcast_msgs_chunked(
buttons: list[ReplyButton] = None,
convo_qs: QuerySet[Conversation],
bi: BotIntegration,
medium: str,
):
convo_ids = list(convo_qs.values_list("id", flat=True))
for i in range(0, len(convo_ids), 100):
Expand All @@ -164,6 +166,7 @@ def send_broadcast_msgs_chunked(
documents=documents,
bi_id=bi.id,
convo_ids=convo_ids[i : i + 100],
medium=medium,
)


Expand All @@ -177,6 +180,7 @@ def send_broadcast_msg(
documents: list[str] = None,
bi_id: int,
convo_ids: list[int],
medium: str,
):
bi = BotIntegration.objects.get(id=bi_id)
convos = Conversation.objects.filter(id__in=convo_ids)
Expand Down Expand Up @@ -205,6 +209,11 @@ def send_broadcast_msg(
username=bi.name,
token=bi.slack_access_token,
)[0]
case Platform.TWILIO:
if medium == "Voice Call":
create_voice_call(convo, text, audio)
else:
send_sms_message(convo, text, media_url=audio)
case _:
raise NotImplementedError(
f"Platform {bi.platform} doesn't support broadcasts yet"
Expand Down
Loading