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 Botintegration Changes Without UI #400

Merged
merged 16 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions bots/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@
Workflow,
)
from bots.tasks import create_personal_channels_for_all_members
from celeryapp.tasks import runner_task
from daras_ai_v2.fastapi_tricks import get_route_url
from daras_ai_v2.fastapi_tricks import get_app_route_url
from gooeysite.custom_actions import export_to_excel, export_to_csv
from gooeysite.custom_filters import (
related_json_field_summary,
Expand Down Expand Up @@ -71,6 +70,19 @@
"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_use_missed_call",
"twilio_tts_voice",
"twilio_asr_language",
"twilio_initial_text",
"twilio_initial_audio_url",
"twilio_waiting_text",
"twilio_waiting_audio_url",
]


class BotIntegrationAdminForm(forms.ModelForm):
Expand Down Expand Up @@ -133,12 +145,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 +207,7 @@ class BotIntegrationAdmin(admin.ModelAdmin):
*wa_fields,
*slack_fields,
*web_fields,
*twilio_fields,
]
},
),
Expand Down Expand Up @@ -256,12 +271,14 @@ def view_analysis_results(self, bi: BotIntegration):

@admin.display(description="Integration Stats")
def api_integration_stats_url(self, bi: BotIntegration):
if not bi.id:
raise bi.DoesNotExist

integration_id = bi.api_integration_id()
return open_in_new_tab(
url=get_route_url(
url=get_app_route_url(
integrations_stats_route,
params=dict(
path_params=dict(
page_slug=VideoBotsPage.slug_versions[-1],
integration_id=integration_id,
),
Expand Down Expand Up @@ -489,6 +506,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Generated by Django 4.2.7 on 2024-07-16 17:01

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


class Migration(migrations.Migration):

dependencies = [
('bots', '0077_savedrun_error_code_savedrun_error_type_and_more'),
]

operations = [
migrations.AlterUniqueTogether(
name='botintegration',
unique_together={('slack_channel_id', 'slack_team_id')},
),
migrations.AddField(
model_name='botintegration',
name='twilio_account_sid',
field=models.TextField(blank=True, default='', help_text='Account SID, required if using api_key to authenticate'),
),
migrations.AddField(
model_name='botintegration',
name='twilio_asr_language',
field=models.TextField(blank=True, default='', 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_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_password',
field=models.TextField(blank=True, default='', help_text='Password to authenticate with, auth_token (if using account_sid) or api_secret (if using api_key)'),
),
migrations.AddField(
model_name='botintegration',
name='twilio_phone_number',
field=phonenumber_field.modelfields.PhoneNumberField(blank=True, default=None, help_text='Twilio phone number as found on twilio.com/console/phone-numbers/incoming (mandatory)', max_length=128, null=True, region=None, unique=True),
),
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'),
),
migrations.AddField(
model_name='botintegration',
name='twilio_tts_voice',
field=models.TextField(blank=True, default='', 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_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_username',
field=models.TextField(blank=True, default='', help_text='Username to authenticate with, either account_sid or api_key'),
),
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'),
),
migrations.AlterUniqueTogether(
name='botintegration',
unique_together={('slack_channel_id', 'slack_team_id'), ('twilio_phone_number', 'twilio_account_sid')},
),
migrations.AddIndex(
model_name='conversation',
index=models.Index(fields=['bot_integration', 'twilio_phone_number'], name='bots_conver_bot_int_73ac7b_idx'),
),
]
115 changes: 110 additions & 5 deletions 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 @@ -365,17 +368,18 @@ def submit_api_call(

# run in a thread to avoid messing up threadlocals
with ThreadPool(1) as pool:
page_cls = Workflow(self.workflow).page_cls
if parent_pr and parent_pr.saved_run == self:
# avoid passing run_id and uid for examples
query_params = dict(example_id=parent_pr.published_run_id)
else:
query_params = dict(
query_params = page_cls.clean_query_params(
example_id=self.example_id, run_id=self.run_id, uid=self.uid
)
page, result, run_id, uid = pool.apply(
submit_api_call,
kwds=dict(
page_cls=Workflow(self.workflow).page_cls,
page_cls=page_cls,
query_params=query_params,
user=current_user,
request_body=request_body,
Expand Down Expand Up @@ -639,6 +643,68 @@ class BotIntegration(models.Model):
help_text="Extra configuration for the bot's web integration",
)

twilio_phone_number = PhoneNumberField(
blank=True,
null=True,
default=None,
unique=True,
help_text="Twilio 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",
)
twilio_account_sid = models.TextField(
blank=True,
default="",
help_text="Account SID, required if using api_key to authenticate",
)
twilio_username = models.TextField(
blank=True,
default="",
help_text="Username to authenticate with, either account_sid or api_key",
)
twilio_password = models.TextField(
blank=True,
default="",
help_text="Password to authenticate with, auth_token (if using account_sid) or api_secret (if using api_key)",
)
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_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_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_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_tts_voice = models.TextField(
default="",
blank=True,
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_asr_language = models.TextField(
default="",
blank=True,
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 All @@ -653,6 +719,7 @@ class Meta:
ordering = ["-updated_at"]
unique_together = [
("slack_channel_id", "slack_team_id"),
("twilio_phone_number", "twilio_account_sid"),
]
indexes = [
models.Index(fields=["billing_account_uid", "platform"]),
Expand Down Expand Up @@ -684,6 +751,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 All @@ -693,7 +761,7 @@ def get_display_name(self):

get_display_name.short_description = "Bot"

def api_integration_id(self):
def api_integration_id(self) -> str:
from routers.bots_api import api_hashids

return api_hashids.encode(self.id)
Expand All @@ -716,6 +784,30 @@ 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

def get_twilio_client(self):
import twilio.rest

return twilio.rest.Client(
account_sid=self.twilio_account_sid or settings.TWILIO_ACCOUNT_SID,
username=self.twilio_username or settings.TWILIO_API_KEY_SID,
password=self.twilio_password or settings.TWILIO_API_KEY_SECRET,
)


class BotIntegrationAnalysisRun(models.Model):
bot_integration = models.ForeignKey(
Expand Down Expand Up @@ -783,10 +875,15 @@ class ConvoState(models.IntegerChoices):


class ConversationQuerySet(models.QuerySet):
def get_unique_users(self) -> "ConversationQuerySet":
def get_unique_users(self) -> models.QuerySet["Conversation"]:
"""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 @@ -979,6 +1076,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 +1110,7 @@ class Meta:
"slack_channel_is_personal",
],
),
models.Index(fields=["bot_integration", "twilio_phone_number"]),
models.Index(fields=["-created_at", "bot_integration"]),
]

Expand All @@ -1024,6 +1128,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
Loading