From a3087a8ccc33f1df7b89072e94eeeb9faa1da259 Mon Sep 17 00:00:00 2001 From: Alexander Metzger Date: Thu, 29 Feb 2024 18:14:57 -0800 Subject: [PATCH] tweaks and fixes --- daras_ai_v2/base.py | 22 +- daras_ai_v2/bot_integration_widgets.py | 2 +- recipes/VideoBots.py | 273 +++++++++++++++---------- routers/facebook_api.py | 75 ++++--- routers/slack_api.py | 75 +++---- 5 files changed, 273 insertions(+), 174 deletions(-) diff --git a/daras_ai_v2/base.py b/daras_ai_v2/base.py index ecd44a77d..c013cc134 100644 --- a/daras_ai_v2/base.py +++ b/daras_ai_v2/base.py @@ -296,7 +296,11 @@ def _render_header(self): if tbreadcrumbs.has_breadcrumbs() or self.run_user: # only render title here if the above row was not empty self._render_title(tbreadcrumbs.h1_title) - if published_run and published_run.notes: + if ( + published_run + and published_run.notes + and MenuTabs.integrations != self.tab + ): st.write(published_run.notes) elif is_root_example: st.write(self.preview_description(current_run.to_dict())) @@ -309,6 +313,17 @@ def can_user_edit_run(self, current_run: SavedRun | None = None) -> bool: and current_run.uid == self.request.user.uid ) + def can_user_edit_published_run( + self, published_run: PublishedRun | None = None + ) -> bool: + published_run = published_run or self.get_current_published_run() + return self.is_current_user_admin() or bool( + published_run + and self.request + and self.request.user + and published_run.created_by == self.request.user + ) + def _render_title(self, title: str): st.write(f"# {title}") @@ -338,6 +353,7 @@ def _render_published_run_buttons( *, current_run: SavedRun, published_run: PublishedRun, + redirect_to: str | None = None, ): is_update_mode = ( self.is_current_user_admin() @@ -395,6 +411,7 @@ def _render_published_run_buttons( published_run=published_run, modal=publish_modal, is_update_mode=is_update_mode, + redirect_to=redirect_to, ) def _render_publish_modal( @@ -404,6 +421,7 @@ def _render_publish_modal( published_run: PublishedRun, modal: Modal, is_update_mode: bool = False, + redirect_to: str | None = None, ): if published_run.is_root() and self.is_current_user_admin(): with st.div(className="text-danger"): @@ -507,7 +525,7 @@ def _render_publish_modal( notes=published_run_notes.strip(), visibility=published_run_visibility, ) - force_redirect(published_run.get_app_url()) + force_redirect(redirect_to or published_run.get_app_url()) def _validate_published_run_title(self, title: str): if slugify(title) in settings.DISALLOWED_TITLE_SLUGS: diff --git a/daras_ai_v2/bot_integration_widgets.py b/daras_ai_v2/bot_integration_widgets.py index b8e5f36c6..53feec596 100644 --- a/daras_ai_v2/bot_integration_widgets.py +++ b/daras_ai_v2/bot_integration_widgets.py @@ -135,7 +135,7 @@ def broadcast_input(bi: BotIntegration): ) text = st.text_area( f""" - #### Broadcast Message 📢 + ###### Broadcast Message 📢 Broadcast a message to all users of this integration using this bot account. \\ You can also do this via the [API]({api_docs_url}) which allows filtering by phone number and more! """, diff --git a/recipes/VideoBots.py b/recipes/VideoBots.py index 560bd3728..f2001768a 100644 --- a/recipes/VideoBots.py +++ b/recipes/VideoBots.py @@ -921,50 +921,66 @@ def render_selected_tab(self, selected_tab): super().render_selected_tab(selected_tab) if selected_tab == MenuTabs.integrations: + st.newline() + # not signed in case if not self.request.user or self.request.user.is_anonymous: self.integration_welcome_screen() + st.newline() + with st.center(): + st.anchor( + "Get Started", + href=self.get_auth_url(self.app_url(query_params={})), + type="primary", + ) return - # signed in but not on a run the user can edit - if not self.can_user_edit_run(): + current_run, published_run = self.get_runs_from_query_params( + *extract_query_params(gooey_get_query_params()) + ) # type: ignore + + # signed in but not on a run the user can edit (admins will never see this) + if not self.can_user_edit_run(current_run): self.integration_welcome_screen( title="Create your Saved Copilot", - get_started_text="🏃🏽‍♂️ Run & Save this Copilot", ) + st.newline() + with st.center(): + st.anchor( + "Run & Save this Copilot", + href=self.get_auth_url(self.app_url(query_params={})), + type="primary", + ) return - current_run, published_run = self.get_runs_from_query_params( - *extract_query_params(gooey_get_query_params()) - ) # type: ignore - - # automatically connect to all the user's unconnected integrations - # TODO: fix - unconnected_q = Q(saved_run=None) | Q(published_run=None) - unconnected_q &= Q(billing_account_uid=self.request.user.uid) - for bi in BotIntegration.objects.filter(unconnected_q): - bi.streaming_enabled = True - bi.user_language = ( - st.session_state.get("user_language") or bi.user_language + # signed, has submitted run, but not published (admins will never see this) + # note: this means we no longer allow botintegrations on non-published runs which is a breaking change requested by Sean + if not self.can_user_edit_published_run(published_run): + self.integration_welcome_screen( + title="Save your Published Copilot", ) - bi.saved_run = current_run - if published_run and published_run.saved_run_id == current_run.id: - bi.published_run = published_run - else: - bi.published_run = None - if bi.platform == Platform.SLACK: - from daras_ai_v2.slack_bot import send_confirmation_msg - - send_confirmation_msg(bi) - bi.save() - - integrations_q = Q(saved_run=current_run) - if published_run and published_run.saved_run_id == current_run.id: - integrations_q |= Q(published_run=published_run) - if published_run.published_run_id: - integrations_q |= Q( - saved_run__example_id=published_run.published_run_id + st.newline() + with st.center(): + self._render_published_run_buttons( + current_run=current_run, + published_run=published_run, + redirect_to=self.get_tab_url(MenuTabs.integrations), ) + return + + # if we come from an integration redirect, we connect the integrations + if "connect_ids" in self.request.query_params: + self.integrations_on_connect( + self.request.query_params.getlist("connect_ids"), + current_run, + published_run, + ) + + # see which integrations are available to the user for the current published run + assert published_run, "At this point, published_run should be available" + integrations_q = Q(published_run=published_run) | Q( + saved_run__example_id=published_run.published_run_id + ) if not self.is_current_user_admin(): integrations_q &= Q(billing_account_uid=self.request.user.uid) @@ -982,11 +998,39 @@ def render_selected_tab(self, selected_tab): integrations, current_run, published_run ) - def integration_welcome_screen( - self, title="Connect your Copilot", get_started_text="🏃🏽‍♂️ Get Started" - ): + def integrations_on_connect(self, ids: list[int], current_run, published_run): + from app_users.models import AppUser + from daras_ai_v2.base import RedirectException + from daras_ai_v2.slack_bot import send_confirmation_msg + + for bid in self.request.query_params.getlist("connect_ids"): + bi = BotIntegration.objects.filter(id=bid).first() + if not bi: + continue + if bi.saved_run is not None: + + st.write( + f"{bi.name} is already connected to a different run by {AppUser.objects.filter(uid=bi.billing_account_uid)}. Please disconnect it first." + ) + continue + + bi.streaming_enabled = True + bi.user_language = st.session_state.get("user_language") or bi.user_language + bi.saved_run = current_run + if published_run and published_run.saved_run_id == current_run.id: + bi.published_run = published_run + else: + bi.published_run = None + if bi.platform == Platform.SLACK: + + send_confirmation_msg(bi) + bi.save() + + raise RedirectException(self.get_tab_url(MenuTabs.integrations)) + + def integration_welcome_screen(self, title="Connect your Copilot"): with st.center(): - st.markdown(f"## {title}") + st.markdown(f"#### {title}") col1, col2, col3 = st.columns( 3, @@ -1018,14 +1062,6 @@ def integration_welcome_screen( st.markdown("3. Test, Analyze & Iterate") st.caption("Analyze your usage. Update your Saved Run to test changes.") - st.newline() - with st.center(): - st.anchor( - get_started_text, - href=self.get_auth_url(self.app_url(query_params={})), - type="primary", - ) - def integration_connect_screen( self, title="Connect your Copilot", status: str | None = None ): @@ -1033,18 +1069,19 @@ def integration_connect_screen( from routers.slack_api import slack_connect_url show_landbot = self.request.query_params.get("show-landbot") == "true" + on_connect = self.get_tab_url(MenuTabs.integrations) with st.center(): st.markdown( f""" - ## {title} + #### {title} {status or f'Run Saved ✅ ~ Connect ~ Test & Configure'} """, unsafe_allow_html=True, ) LINKSTYLE = 'class="btn btn-theme btn-secondary" style="margin: 0; display: flex; justify-content: center; align-items: center; padding: 8px; border: 1px solid black; min-width: 164px; width: 200px; aspect-ratio: 5 / 2; overflow: hidden; border-radius: 10px" draggable="false"' - IMGSTYLE = 'style="width: 100%" draggable="false"' + IMGSTYLE = 'style="width: 80%" draggable="false"' ROWSTYLE = 'style="display: flex; align-items: center; gap: 1em; margin-bottom: 1rem" draggable="false"' DESCRIPTIONSTYLE = f'style="color: {GRAYCOLOR}; text-align: left"' st.markdown( @@ -1052,25 +1089,25 @@ def integration_connect_screen( f"""
- + Whatsapp
Bring your own WhatsApp number to connect. Need a new one? Email sales@gooey.ai.
- + Slack
Connect to a Slack Channel. Help Guide.
- + Facebook Messenger
Connect to a Facebook Page you own. Help Guide.
- + Instagram
Connect to an Instagram account you own.
@@ -1129,121 +1166,133 @@ def integration_test_config_screen( with st.center(): st.markdown( f""" - ## Configure your Copilot + #### Configure your Copilot Run Saved ✅ ~ Connected ✅ ~ Test & Configure ✅ """, unsafe_allow_html=True, ) - with st.center(direction="row"): - bid = st.horizontal_radio( - "", - [i.id for i in integrations] + [None], - lambda i: ( - f' Add' - if not i - else { - i.id: f' {Platform(i.platform).label}' + if integrations.count() > 1: + with st.center(direction="row"): + bid = st.horizontal_radio( + "", + [i.id for i in integrations], + lambda i: { + i.id: f' {i.name}' for i in integrations - }[i] - ), - key="bi_id", - button_props={"style": {"width": "140px"}}, - ) - if bid is None: - raise RedirectException(add_integration) - bi = integrations.get(id=bid) - icon = f'' + }[i], + key="bi_id", + button_props={"style": {"width": "156px", "overflow": "hidden"}}, + ) + bi = integrations.get(id=bid) + icon = f'' + st.change_url( + self.get_tab_url( + MenuTabs.integrations, query_params={"bi_id": bi.id} + ), + self.request, + ) + else: + bi = integrations[0] + icon = f'' st.newline() with st.center(): with st.div(style={"width": "100%", "text-align": "left"}): + test_link = get_bot_test_link(bi) col1, col2 = st.columns(2, style={"align-items": "center"}) with col1: + st.write("###### Connected To") st.write(f"{icon} {bi}", unsafe_allow_html=True) with col2: - copy_to_clipboard_button( - "Copy Link", - value=self.get_tab_url( - MenuTabs.integrations, query_params={"bi_id": bi.id} - ), - type="link", - ) + if test_link: + copy_to_clipboard_button( + f"Copy {Platform(bi.platform).label} Link", + value=test_link, + type="secondary", + ) + else: + st.write("Message quicklink not available.") - st.newline() col1, col2 = st.columns(2, style={"align-items": "center"}) with col1: - st.write("#### Test 📱") + st.write("###### Test") st.caption(f"Send a test {Platform(bi.platform).label} message.") with col2: - test_link = get_bot_test_link(bi) if test_link: st.anchor( f"{icon} Message {bi.get_display_name()}", test_link, unsafe_allow_html=True, - style={"width": "100%"}, ) else: st.write("Message quicklink not available.") col1, col2 = st.columns(2, style={"align-items": "center"}) with col1: - st.write("#### Understand your Users 📊") - st.caption( - f"Configure your {Platform(bi.platform).label} integration." - ) + st.write("###### Understand your Users") + st.caption(f"See real-time analytics.") with col2: stats_url = furl( VideoBotsStatsPage.app_url(), args={"bi_id": bi.id} ).tostr() st.anchor( - "View Analytics", + "📊 View Analytics", stats_url, - style={"width": "100%"}, ) - col1, col2 = st.columns(2, style={"align-items": "center"}) - with col1: - st.write("#### Evaluate ⚖️") - st.caption(f"Run automated tests against sample user messages.") - with col2: - st.anchor( - "Run Bulk Tests", - BulkRunnerPage.app_url(), - style={"width": "100%"}, - ) + # ==== future changes ==== + # col1, col2 = st.columns(2, style={"align-items": "center"}) + # with col1: + # st.write("###### Evaluate ⚖️") + # st.caption(f"Run automated tests against sample user messages.") + # with col2: + # st.anchor( + # "Run Bulk Tests", + # BulkRunnerPage.app_url(), + # ) # st.write("#### Automated Analysis 🧠") # st.caption( # "Add a Gooey.AI LLM prompt to automatically analyse and categorize user messages. [Example](https://gooey.ai/compare-large-language-models/how-farmerchat-turns-conversations-to-structured-data/?example_id=lbjnoem7) and [Guide](https://gooey.ai/docs/guides/copilot/conversation-analysis)." # ) - st.newline() - st.write("#### Configure Settings 🛠️") - if bi.platform == Platform.SLACK: - slack_specific_settings(bi, run_title) - general_integration_settings(bi) + with st.expander("Configure Settings 🛠️"): + if bi.platform == Platform.SLACK: + slack_specific_settings(bi, run_title) + general_integration_settings(bi) - if bi.platform in [Platform.SLACK, Platform.WHATSAPP]: - st.newline() - st.newline() - broadcast_input(bi) + if bi.platform in [Platform.SLACK, Platform.WHATSAPP]: + st.newline() + st.newline() + broadcast_input(bi) - st.newline() - st.newline() - st.write("#### Danger Zone 🚨") col1, col2 = st.columns(2, style={"align-items": "center"}) with col1: + st.write("###### Disconnect") + st.caption( + f"Disconnect {run_title} from {Platform(bi.platform).label} {bi.get_display_name()}." + ) + with col2: if st.button( - "🔌💔️ Disconnect", key="btn_disconnect", style={"width": "100%"} + "💔️ Disconnect", + key="btn_disconnect", ): bi.saved_run = None bi.published_run = None + bi.save() + st.experimental_rerun() + + col1, col2 = st.columns(2, style={"align-items": "center"}) + with col1: + st.write("###### Add Integration") + st.caption(f"Add another connection for {run_title}.") with col2: - st.caption( - f"Disconnect {run_title} from {Platform(bi.platform).label} {bi.get_display_name()}" - ) + if st.button( + f'   Add Integration', + key="btn_connect", + ): + raise RedirectException(add_integration) def show_landbot_widget(): diff --git a/routers/facebook_api.py b/routers/facebook_api.py index 134415405..41679b72b 100644 --- a/routers/facebook_api.py +++ b/routers/facebook_api.py @@ -23,7 +23,8 @@ def fb_connect_whatsapp_redirect(request: Request): redirect_url = furl("/login", query_params={"next": request.url}) return RedirectResponse(str(redirect_url)) - retry_button = f'Retry' + on_completion = request.query_params.get("state") + retry_button = f'Retry' code = request.query_params.get("code") if not code: @@ -63,6 +64,7 @@ def fb_connect_whatsapp_redirect(request: Request): # {'data': [{'verified_name': 'XXXX', 'code_verification_status': 'VERIFIED', 'display_phone_number': 'XXXX', 'quality_rating': 'UNKNOWN', 'platform_type': 'NOT_APPLICABLE', 'throughput': {'level': 'NOT_APPLICABLE'}, 'last_onboarded_time': '2024-02-22T20:42:16+0000', 'id': 'XXXX'}], 'paging': {'cursors': {'before': 'XXXX', 'after': 'XXXX'}}} phone_numbers = r.json()["data"] + integrations = [] for phone_number in phone_numbers: business_name = phone_number["verified_name"] display_phone_number = phone_number["display_phone_number"] @@ -109,7 +111,9 @@ def fb_connect_whatsapp_redirect(request: Request): ) r.raise_for_status() - return HTMLResponse( + integrations.append(bi) + + return return_to_app_url(integrations, on_completion) or HTMLResponse( f"Sucessfully Connected to whatsapp! You may now close this page." ) @@ -120,7 +124,8 @@ def fb_connect_redirect(request: Request): redirect_url = furl("/login", query_params={"next": request.url}) return RedirectResponse(str(redirect_url)) - retry_button = f'Retry' + on_completion = request.query_params.get("state") + retry_button = f'Retry' code = request.query_params.get("code") if not code: @@ -129,7 +134,7 @@ def fb_connect_redirect(request: Request): + retry_button, status_code=400, ) - user_access_token = _get_access_token_from_code(code) + user_access_token = _get_access_token_from_code(code, fb_connect_redirect_url) db.get_user_doc_ref(request.user.uid).update({"fb_access_token": user_access_token}) @@ -146,7 +151,7 @@ def fb_connect_redirect(request: Request): ) page_names = ", ".join(map(str, integrations)) - return HTMLResponse( + return return_to_app_url(integrations, on_completion) or HTMLResponse( f"Sucessfully Connected to {page_names}! You may now close this page." ) @@ -213,13 +218,29 @@ def fb_webhook( return Response("OK") -wa_connect_redirect_url = str( - furl(settings.APP_BASE_URL) - / router.url_path_for(fb_connect_whatsapp_redirect.__name__) -) +def return_to_app_url( + integrations: list, + on_completion: str | None = None, +) -> bool | RedirectResponse: + if not on_completion or not integrations: + return False + return RedirectResponse( + furl(on_completion) + .add(query_params={"connect_ids": ",".join([str(i.id) for i in integrations])}) + .tostr() + ) + -wa_connect_url = str( +wa_connect_redirect_url = ( furl( + settings.APP_BASE_URL, + ) + / router.url_path_for(fb_connect_whatsapp_redirect.__name__) +).tostr() + + +def wa_connect_url(on_completion: str | None = None) -> str: + return furl( "https://www.facebook.com/v18.0/dialog/oauth", query_params={ "client_id": settings.FB_APP_ID, @@ -227,16 +248,21 @@ def fb_webhook( "redirect_uri": wa_connect_redirect_url, "response_type": "code", "config_id": settings.FB_WHATSAPP_CONFIG_ID, + "state": on_completion, }, - ) -) + ).tostr() -fb_connect_redirect_url = str( - furl(settings.APP_BASE_URL) / router.url_path_for(fb_connect_redirect.__name__) -) -fb_connect_url = str( +fb_connect_redirect_url = ( furl( + settings.APP_BASE_URL, + ) + / router.url_path_for(fb_connect_redirect.__name__) +).tostr() + + +def fb_connect_url(on_completion: str | None = None) -> str: + return furl( "https://www.facebook.com/dialog/oauth", query_params={ "client_id": settings.FB_APP_ID, @@ -250,12 +276,13 @@ def fb_webhook( "pages_show_list", ] ), + "state": on_completion, }, - ) -) + ).tostr() -ig_connect_url = str( - furl( + +def ig_connect_url(on_completion: str | None = None) -> str: + return furl( "https://www.facebook.com/dialog/oauth", query_params={ "client_id": settings.FB_APP_ID, @@ -272,17 +299,17 @@ def fb_webhook( "pages_show_list", ] ), + "state": on_completion, }, - ) -) + ).tostr() -def _get_access_token_from_code(code: str, redirect_uri: str | None = None) -> str: +def _get_access_token_from_code(code: str, redirect_uri: str) -> str: r = requests.get( "https://graph.facebook.com/v15.0/oauth/access_token", params={ "client_id": settings.FB_APP_ID, - "redirect_uri": redirect_uri or fb_connect_redirect_url, + "redirect_uri": redirect_uri, "client_secret": settings.FB_APP_SECRET, "code": code, }, diff --git a/routers/slack_api.py b/routers/slack_api.py index 4186a474e..d300b70d7 100644 --- a/routers/slack_api.py +++ b/routers/slack_api.py @@ -23,43 +23,47 @@ fetch_user_info, parse_slack_response, ) +from routers.facebook_api import return_to_app_url router = APIRouter() -slack_connect_url = furl( - "https://slack.com/oauth/v2/authorize", - query_params=dict( - client_id=settings.SLACK_CLIENT_ID, - scope=",".join( - [ - "channels:history", - "channels:read", - "chat:write", - "chat:write.customize", - "files:read", - "files:write", - "groups:history", - "groups:read", - "groups:write", - "groups:write.invites", - "groups:write.topic", - "incoming-webhook", - "remote_files:read", - "remote_files:share", - "remote_files:write", - "users:read", - ] - ), - user_scope=",".join( - [ - "channels:write", - "channels:write.invites", - "groups:write", - "groups:write.invites", - ] + +def slack_connect_url(on_completion: str | None = None): + return furl( + "https://slack.com/oauth/v2/authorize", + query_params=dict( + client_id=settings.SLACK_CLIENT_ID, + scope=",".join( + [ + "channels:history", + "channels:read", + "chat:write", + "chat:write.customize", + "files:read", + "files:write", + "groups:history", + "groups:read", + "groups:write", + "groups:write.invites", + "groups:write.topic", + "incoming-webhook", + "remote_files:read", + "remote_files:share", + "remote_files:write", + "users:read", + ] + ), + user_scope=",".join( + [ + "channels:write", + "channels:write.invites", + "groups:write", + "groups:write.invites", + ] + ), + state=on_completion, ), - ), -) + ) @router.get("/__/slack/redirect/") @@ -68,7 +72,8 @@ def slack_connect_redirect(request: Request): redirect_url = furl("/login", query_params={"next": request.url}) return RedirectResponse(str(redirect_url)) - retry_button = f'Retry' + on_completion = request.query_params.get("state") + retry_button = f'Retry' code = request.query_params.get("code") if not code: @@ -134,7 +139,7 @@ def slack_connect_redirect(request: Request): if bi.slack_create_personal_channels: create_personal_channels_for_all_members.delay(bi.id) - return HTMLResponse( + return return_to_app_url([bi], on_completion) or HTMLResponse( f"Sucessfully Connected to {slack_team_name} workspace on #{slack_channel_name}! You may now close this page." )