diff --git a/README.md b/README.md index 89b0c5c34..a2fbe6a7a 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,12 @@ ngrok http 8080 5. Copy the temporary access token there and set env var `WHATSAPP_ACCESS_TOKEN = XXXX` +**(Optional) Use the test script to send yourself messages** + +```bash +python manage.py runscript test_wa_msg_send --script-args 104696745926402 +918764022384 +``` +Replace `+918764022384` with your number and `104696745926402` with the test number ID ## Dangerous postgres commands @@ -178,3 +184,4 @@ rsync -P -a @captain.us-1.gooey.ai:/home//fixture.json . createdb -T template0 $PGDATABASE pg_dump $SOURCE_DATABASE | psql -q $PGDATABASE ``` + diff --git a/bots/admin.py b/bots/admin.py index 49321ba35..0b6b475cc 100644 --- a/bots/admin.py +++ b/bots/admin.py @@ -168,6 +168,7 @@ class BotIntegrationAdmin(admin.ModelAdmin): "Settings", { "fields": [ + "streaming_enabled", "show_feedback_buttons", "analysis_run", "view_analysis_results", @@ -491,6 +492,7 @@ class MessageAdmin(admin.ModelAdmin): "prev_msg_content", "prev_msg_display_content", "prev_msg_saved_run", + "response_time", ] ordering = ["created_at"] actions = [export_to_csv, export_to_excel] @@ -550,6 +552,7 @@ def get_fieldsets(self, request, msg: Message = None): "Analysis", { "fields": [ + "response_time", "analysis_result", "analysis_run", "question_answered", diff --git a/bots/migrations/0056_botintegration_streaming_enabled.py b/bots/migrations/0056_botintegration_streaming_enabled.py new file mode 100644 index 000000000..dcf1366fa --- /dev/null +++ b/bots/migrations/0056_botintegration_streaming_enabled.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.7 on 2024-01-31 19:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("bots", "0055_workflowmetadata"), + ] + + operations = [ + migrations.AddField( + model_name="botintegration", + name="streaming_enabled", + field=models.BooleanField( + default=False, + help_text="If set, the bot will stream messages to the frontend", + ), + ), + ] diff --git a/bots/migrations/0057_message_response_time_and_more.py b/bots/migrations/0057_message_response_time_and_more.py new file mode 100644 index 000000000..5bdecd39c --- /dev/null +++ b/bots/migrations/0057_message_response_time_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.7 on 2024-02-05 15:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bots', '0056_botintegration_streaming_enabled'), + ] + + operations = [ + migrations.AddField( + model_name='message', + name='response_time', + field=models.DurationField(default=None, help_text='The time it took for the bot to respond to the corresponding user message', null=True), + ), + migrations.AlterField( + model_name='botintegration', + name='streaming_enabled', + field=models.BooleanField(default=False, help_text='If set, the bot will stream messages to the frontend (Slack only)'), + ), + ] diff --git a/bots/models.py b/bots/models.py index fd6071f58..8e2a41dc8 100644 --- a/bots/models.py +++ b/bots/models.py @@ -571,6 +571,11 @@ class BotIntegration(models.Model): help_text="If provided, the message content will be analyzed for this bot using this saved run", ) + streaming_enabled = models.BooleanField( + default=False, + help_text="If set, the bot will stream messages to the frontend (Slack only)", + ) + created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -712,7 +717,26 @@ def to_df_format( "Bot": str(convo.bot_integration), } rows.append(row) - df = pd.DataFrame.from_records(rows) + df = pd.DataFrame.from_records( + rows, + columns=[ + "Name", + "Messages", + "Correct Answers", + "Thumbs up", + "Thumbs down", + "Last Sent", + "First Sent", + "A7", + "A30", + "R1", + "R7", + "R30", + "Delta Hours", + "Created At", + "Bot", + ], + ) return df @@ -902,13 +926,30 @@ def to_df_format( "Sent": message.created_at.astimezone(tz) .replace(tzinfo=None) .strftime("%b %d, %Y %I:%M %p"), - "Feedback": message.feedbacks.first().get_display_text() - if message.feedbacks.first() - else None, # only show first feedback as per Sean's request + "Feedback": ( + message.feedbacks.first().get_display_text() + if message.feedbacks.first() + else None + ), # only show first feedback as per Sean's request "Analysis JSON": message.analysis_result, + "Response Time": ( + message.response_time + and round(message.response_time.total_seconds(), 1) + ), } rows.append(row) - df = pd.DataFrame.from_records(rows) + df = pd.DataFrame.from_records( + rows, + columns=[ + "Name", + "Role", + "Message (EN)", + "Sent", + "Feedback", + "Analysis JSON", + "Response Time", + ], + ) return df def to_df_analysis_format( @@ -916,21 +957,24 @@ def to_df_analysis_format( ) -> "pd.DataFrame": import pandas as pd - qs = self.filter(role=CHATML_ROLE_USER).prefetch_related("feedbacks") + qs = self.filter(role=CHATML_ROLE_ASSISSTANT).prefetch_related("feedbacks") rows = [] for message in qs[:row_limit]: message: Message row = { "Name": message.conversation.get_display_name(), - "Question (EN)": message.content, - "Answer (EN)": message.get_next_by_created_at().content, + "Question (EN)": message.get_previous_by_created_at().content, + "Answer (EN)": message.content, "Sent": message.created_at.astimezone(tz) .replace(tzinfo=None) .strftime("%b %d, %Y %I:%M %p"), "Analysis JSON": message.analysis_result, } rows.append(row) - df = pd.DataFrame.from_records(rows) + df = pd.DataFrame.from_records( + rows, + columns=["Name", "Question (EN)", "Answer (EN)", "Sent", "Analysis JSON"], + ) return df def as_llm_context(self, limit: int = 100) -> list["ConversationEntry"]: @@ -1012,6 +1056,12 @@ class Message(models.Model): help_text="Subject of given question (DEPRECATED)", ) + response_time = models.DurationField( + default=None, + null=True, + help_text="The time it took for the bot to respond to the corresponding user message", + ) + _analysis_started = False objects = MessageQuerySet.as_manager() @@ -1115,7 +1165,20 @@ def to_df_format( "Question Answered": feedback.message.question_answered, } rows.append(row) - df = pd.DataFrame.from_records(rows) + df = pd.DataFrame.from_records( + rows, + columns=[ + "Name", + "Question (EN)", + "Question Sent", + "Answer (EN)", + "Answer Sent", + "Rating", + "Feedback (EN)", + "Feedback Sent", + "Question Answered", + ], + ) return df diff --git a/bots/tasks.py b/bots/tasks.py index 9afe2c343..5dda17e90 100644 --- a/bots/tasks.py +++ b/bots/tasks.py @@ -128,7 +128,7 @@ def send_broadcast_msg( channel_is_personal=convo.slack_channel_is_personal, username=bi.name, token=bi.slack_access_token, - ) + )[0] case _: raise NotImplementedError( f"Platform {bi.platform} doesn't support broadcasts yet" diff --git a/bots/tests.py b/bots/tests.py index 21eef78f8..30105e648 100644 --- a/bots/tests.py +++ b/bots/tests.py @@ -92,3 +92,66 @@ def test_create_bot_integration_conversation_message(transactional_db): assert message_b.role == CHATML_ROLE_ASSISSTANT assert message_b.content == "Red, green, and yellow grow the best." assert message_b.display_content == "Red, green, and yellow grow the best." + + +def test_stats_get_tabular_data_invalid_sorting_options(transactional_db): + from recipes.VideoBotsStats import VideoBotsStatsPage + + page = VideoBotsStatsPage() + + # setup + run_url = "https://my_run_url" + bi = BotIntegration.objects.create( + name="My Bot Integration", + saved_run=None, + billing_account_uid="fdnacsFSBQNKVW8z6tzhBLHKpAm1", # digital green's account id + user_language="en", + show_feedback_buttons=True, + platform=Platform.WHATSAPP, + wa_phone_number="my_whatsapp_number", + wa_phone_number_id="my_whatsapp_number_id", + ) + convos = Conversation.objects.filter(bot_integration=bi) + msgs = Message.objects.filter(conversation__in=convos) + + # valid option but no data + df = page.get_tabular_data( + bi, run_url, convos, msgs, "Answered Successfully", "Name" + ) + assert df.shape[0] == 0 + assert "Name" in df.columns + + # valid option and data + convo = Conversation.objects.create( + bot_integration=bi, + state=ConvoState.INITIAL, + wa_phone_number="+919876543210", + ) + Message.objects.create( + conversation=convo, + role=CHATML_ROLE_USER, + content="What types of chilies can be grown in Mumbai?", + display_content="What types of chilies can be grown in Mumbai?", + ) + Message.objects.create( + conversation=convo, + role=CHATML_ROLE_ASSISSTANT, + content="Red, green, and yellow grow the best.", + display_content="Red, green, and yellow grow the best.", + analysis_result={"Answered": True}, + ) + convos = Conversation.objects.filter(bot_integration=bi) + msgs = Message.objects.filter(conversation__in=convos) + assert msgs.count() == 2 + df = page.get_tabular_data( + bi, run_url, convos, msgs, "Answered Successfully", "Name" + ) + assert df.shape[0] == 1 + assert "Name" in df.columns + + # invalid sort option should be ignored + df = page.get_tabular_data( + bi, run_url, convos, msgs, "Answered Successfully", "Invalid" + ) + assert df.shape[0] == 1 + assert "Name" in df.columns diff --git a/daras_ai_v2/base.py b/daras_ai_v2/base.py index 7187178b7..f05a0c71c 100644 --- a/daras_ai_v2/base.py +++ b/daras_ai_v2/base.py @@ -189,7 +189,7 @@ def setup_render(self): def refresh_state(self): _, run_id, uid = extract_query_params(gooey_get_query_params()) - channel = f"gooey-outputs/{self.slug_versions[0]}/{uid}/{run_id}" + channel = self.realtime_channel_name(run_id, uid) output = realtime_pull([channel])[0] if output: st.session_state.update(output) @@ -197,7 +197,7 @@ def refresh_state(self): def render(self): self.setup_render() - if self.get_run_state() == RecipeRunState.running: + if self.get_run_state(st.session_state) == RecipeRunState.running: self.refresh_state() else: realtime_clear_subs() @@ -1307,12 +1307,13 @@ def _render_input_col(self): ) return submitted - def get_run_state(self) -> RecipeRunState: - if st.session_state.get(StateKeys.run_status): + @classmethod + def get_run_state(cls, state: dict[str, typing.Any]) -> RecipeRunState: + if state.get(StateKeys.run_status): return RecipeRunState.running - elif st.session_state.get(StateKeys.error_msg): + elif state.get(StateKeys.error_msg): return RecipeRunState.failed - elif st.session_state.get(StateKeys.run_time): + elif state.get(StateKeys.run_time): return RecipeRunState.completed else: # when user is at a recipe root, and not running anything @@ -1331,7 +1332,7 @@ def _render_output_col(self, submitted: bool): self._render_before_output() - run_state = self.get_run_state() + run_state = self.get_run_state(st.session_state) match run_state: case RecipeRunState.completed: self._render_completed_output() @@ -1458,13 +1459,16 @@ def call_runner_task(self, example_id, run_id, uid, is_api_call=False): run_id=run_id, uid=uid, state=st.session_state, - channel=f"gooey-outputs/{self.slug_versions[0]}/{uid}/{run_id}", + channel=self.realtime_channel_name(run_id, uid), query_params=self.clean_query_params( example_id=example_id, run_id=run_id, uid=uid ), is_api_call=is_api_call, ) + def realtime_channel_name(self, run_id, uid): + return f"gooey-outputs/{self.slug_versions[0]}/{uid}/{run_id}" + def generate_credit_error_message(self, example_id, run_id, uid) -> str: account_url = furl(settings.APP_BASE_URL) / "account/" if self.request.user.is_anonymous: diff --git a/daras_ai_v2/bot_integration_widgets.py b/daras_ai_v2/bot_integration_widgets.py index 3880f9fe7..0790f0b43 100644 --- a/daras_ai_v2/bot_integration_widgets.py +++ b/daras_ai_v2/bot_integration_widgets.py @@ -19,11 +19,16 @@ def general_integration_settings(bi: BotIntegration): st.session_state[f"_bi_user_language_{bi.id}"] = BotIntegration._meta.get_field( "user_language" ).default - st.session_state[ - f"_bi_show_feedback_buttons_{bi.id}" - ] = BotIntegration._meta.get_field("show_feedback_buttons").default + st.session_state[f"_bi_show_feedback_buttons_{bi.id}"] = ( + BotIntegration._meta.get_field("show_feedback_buttons").default + ) st.session_state[f"_bi_analysis_url_{bi.id}"] = None + bi.streaming_enabled = st.checkbox( + "**📡 Streaming Enabled**", + value=bi.streaming_enabled, + key=f"_bi_streaming_enabled_{bi.id}", + ) bi.show_feedback_buttons = st.checkbox( "**👍🏾 👎🏽 Show Feedback Buttons**", value=bi.show_feedback_buttons, diff --git a/daras_ai_v2/bots.py b/daras_ai_v2/bots.py index dfc00c794..9f2ebd555 100644 --- a/daras_ai_v2/bots.py +++ b/daras_ai_v2/bots.py @@ -1,9 +1,11 @@ import mimetypes import traceback import typing +from datetime import datetime from urllib.parse import parse_qs from django.db import transaction +from django.utils import timezone from fastapi import HTTPException, Request from furl import furl from sentry_sdk import capture_exception @@ -20,11 +22,13 @@ MessageAttachment, ) from daras_ai_v2.asr import AsrModels, run_google_translate -from daras_ai_v2.base import BasePage +from daras_ai_v2.base import BasePage, RecipeRunState, StateKeys from daras_ai_v2.language_model import CHATML_ROLE_USER, CHATML_ROLE_ASSISTANT from daras_ai_v2.vector_search import doc_url_to_file_metadata +from gooey_ui.pubsub import realtime_subscribe from gooeysite.bg_db_conn import db_middleware from recipes.VideoBots import VideoBotsPage, ReplyButton +from routers.api import submit_api_call PAGE_NOT_CONNECTED_ERROR = ( "💔 Looks like you haven't connected this page to a gooey.ai workflow. " @@ -47,7 +51,7 @@ """.strip() ERROR_MSG = """ -`{0!r}` +`{}` ⚠️ Sorry, I ran into an error while processing your request. Please try again, or type "Reset" to start over. """.strip() @@ -60,6 +64,8 @@ TAPPED_SKIP_MSG = "🌱 Alright. What else can I help you with?" +SLACK_MAX_SIZE = 3000 + async def request_json(request: Request): return await request.json() @@ -80,33 +86,13 @@ class BotInterface: input_type: str language: str show_feedback_buttons: bool = False + streaming_enabled: bool = False + can_update_message: bool = False convo: Conversation recieved_msg_id: str = None input_glossary: str | None = None output_glossary: str | None = None - def send_msg_or_default( - self, - *, - text: str | None = None, - audio: str = None, - video: str = None, - buttons: list[ReplyButton] = None, - documents: list[str] = None, - should_translate: bool = False, - default: str = DEFAULT_RESPONSE, - ): - if not (text or audio or video or documents): - text = default - return self.send_msg( - text=text, - audio=audio, - video=video, - buttons=buttons, - documents=documents, - should_translate=should_translate, - ) - def send_msg( self, *, @@ -116,6 +102,7 @@ def send_msg( buttons: list[ReplyButton] = None, documents: list[str] = None, should_translate: bool = False, + update_msg_id: str = None, ) -> str | None: raise NotImplementedError @@ -166,6 +153,7 @@ def _unpack_bot_integration(self): self.billing_account_uid = bi.billing_account_uid self.language = bi.user_language self.show_feedback_buttons = bi.show_feedback_buttons + self.streaming_enabled = bi.streaming_enabled def get_interactive_msg_info(self) -> tuple[str, str]: raise NotImplementedError("This bot does not support interactive messages.") @@ -202,6 +190,7 @@ def _on_msg(bot: BotInterface): speech_run = None input_images = None input_documents = None + recieved_time: datetime = timezone.now() if not bot.page_cls: bot.send_msg(text=PAGE_NOT_CONNECTED_ERROR) return @@ -286,6 +275,7 @@ def _on_msg(bot: BotInterface): input_documents=input_documents, input_text=input_text, speech_run=speech_run, + recieved_time=recieved_time, ) @@ -324,41 +314,115 @@ def _process_and_send_msg( input_images: list[str] | None, input_documents: list[str] | None, input_text: str, + recieved_time: datetime, speech_run: str | None, ): - try: - # # mock testing - # msgs_to_save, response_audio, response_text, response_video = _echo( - # bot, input_text - # ) - # make API call to gooey bots to get the response - response, url = _process_msg( - page_cls=bot.page_cls, - api_user=billing_account_user, - query_params=bot.query_params, - convo=bot.convo, - input_text=input_text, - user_language=bot.language, - speech_run=speech_run, - input_images=input_images, - input_documents=input_documents, - ) - except HTTPException as e: - traceback.print_exc() - capture_exception(e) - # send error msg as repsonse - bot.send_msg(text=ERROR_MSG.format(e)) - return + # get latest messages for context (upto 100) + saved_msgs = bot.convo.messages.all().as_llm_context() - # send the response to the user - msg_id = bot.send_msg_or_default( - text=response.output_text and response.output_text[0], - audio=response.output_audio and response.output_audio[0], - video=response.output_video and response.output_video[0], - documents=response.output_documents or [], - buttons=_feedback_start_buttons() if bot.show_feedback_buttons else None, + # # mock testing + # result = _mock_api_output(input_text) + page, result, run_id, uid = submit_api_call( + page_cls=bot.page_cls, + user=billing_account_user, + request_body={ + "input_prompt": input_text, + "input_images": input_images, + "input_documents": input_documents, + "messages": saved_msgs, + "user_language": bot.language, + }, + query_params=bot.query_params, ) + if bot.show_feedback_buttons: + buttons = _feedback_start_buttons() + else: + buttons = None + + update_msg_id = None # this is the message id to update during streaming + sent_msg_id = None # this is the message id to record in the db + last_idx = 0 # this is the last index of the text sent to the user + if bot.streaming_enabled: + # subscribe to the realtime channel for updates + channel = page.realtime_channel_name(run_id, uid) + with realtime_subscribe(channel) as realtime_gen: + for state in realtime_gen: + run_state = page.get_run_state(state) + run_status = state.get(StateKeys.run_status) or "" + # check for errors + if run_state == RecipeRunState.failed: + err_msg = state.get(StateKeys.error_msg) + bot.send_msg(text=ERROR_MSG.format(err_msg)) + return # abort + if run_state != RecipeRunState.running: + break # we're done running, abort + text = state.get("output_text") and state.get("output_text")[0] + if not text: + # if no text, send the run status + if bot.can_update_message: + update_msg_id = bot.send_msg( + text=run_status, update_msg_id=update_msg_id + ) + continue # no text, wait for the next update + streaming_done = not run_status.lower().startswith("streaming") + # send the response to the user + if bot.can_update_message: + update_msg_id = bot.send_msg( + text=text.strip() + "...", + update_msg_id=update_msg_id, + buttons=buttons if streaming_done else None, + ) + last_idx = len(text) + else: + next_chunk = text[last_idx:] + last_idx = len(text) + if not next_chunk: + continue # no chunk, wait for the next update + update_msg_id = bot.send_msg( + text=next_chunk, + buttons=buttons if streaming_done else None, + ) + if streaming_done and not bot.can_update_message: + # if we send the buttons, this is the ID we need to record in the db for lookups later when the button is pressed + sent_msg_id = update_msg_id + # don't show buttons again + buttons = None + if streaming_done: + break # we're done streaming, abort + + # wait for the celery task to finish + result.get(disable_sync_subtasks=False) + # get the final state from db + state = page.run_doc_sr(run_id, uid).to_dict() + # check for errors + err_msg = state.get(StateKeys.error_msg) + if err_msg: + bot.send_msg(text=ERROR_MSG.format(err_msg)) + return + + text = (state.get("output_text") and state.get("output_text")[0]) or "" + audio = state.get("output_audio") and state.get("output_audio")[0] + video = state.get("output_video") and state.get("output_video")[0] + documents = state.get("output_documents") or [] + # check for empty response + if not (text or audio or video or documents or buttons): + bot.send_msg(text=DEFAULT_RESPONSE) + return + # if in-place updates are enabled, update the message, otherwise send the remaining text + if not bot.can_update_message: + text = text[last_idx:] + # send the response to the user if there is any remaining + if text or audio or video or documents or buttons: + update_msg_id = bot.send_msg( + text=text, + audio=audio, + video=video, + documents=documents, + buttons=buttons, + update_msg_id=update_msg_id, + ) + # save msgs to db _save_msgs( bot=bot, @@ -366,9 +430,10 @@ def _process_and_send_msg( input_documents=input_documents, input_text=input_text, speech_run=speech_run, - platform_msg_id=msg_id, - response=response, - url=url, + platform_msg_id=sent_msg_id or update_msg_id, + response=VideoBotsPage.ResponseModel.parse_obj(state), + url=page.app_url(run_id=run_id, uid=uid), + received_time=recieved_time, ) @@ -381,6 +446,7 @@ def _save_msgs( platform_msg_id: str | None, response: VideoBotsPage.ResponseModel, url: str, + received_time: datetime, ): # create messages for future context user_msg = Message( @@ -389,11 +455,14 @@ def _save_msgs( role=CHATML_ROLE_USER, content=response.raw_input_text, display_content=input_text, - saved_run=SavedRun.objects.get_or_create( - workflow=Workflow.ASR, **furl(speech_run).query.params - )[0] - if speech_run - else None, + saved_run=( + SavedRun.objects.get_or_create( + workflow=Workflow.ASR, **furl(speech_run).query.params + )[0] + if speech_run + else None + ), + response_time=timezone.now() - received_time, ) attachments = [] for f_url in (input_images or []) + (input_documents or []): @@ -410,6 +479,7 @@ def _save_msgs( saved_run=SavedRun.objects.get_or_create( workflow=Workflow.VIDEO_BOTS, **furl(url).query.params )[0], + response_time=timezone.now() - received_time, ) # save the messages & attachments with transaction.atomic(): @@ -420,45 +490,6 @@ def _save_msgs( assistant_msg.save() -def _process_msg( - *, - page_cls, - api_user: AppUser, - query_params: dict, - convo: Conversation, - input_images: list[str] | None, - input_documents: list[str] | None, - input_text: str, - user_language: str, - speech_run: str | None, -) -> tuple[VideoBotsPage.ResponseModel, str]: - from routers.api import call_api - - # get latest messages for context (upto 100) - saved_msgs = convo.messages.all().as_llm_context() - - # # mock testing - # result = _mock_api_output(input_text) - - # call the api with provided input - result = call_api( - page_cls=page_cls, - user=api_user, - request_body={ - "input_prompt": input_text, - "input_images": input_images, - "input_documents": input_documents, - "messages": saved_msgs, - "user_language": user_language, - }, - query_params=query_params, - ) - # parse result - response = page_cls.ResponseModel.parse_obj(result["output"]) - url = result.get("url", "") - return response, url - - def _handle_interactive_msg(bot: BotInterface): try: button_id, context_msg_id = bot.get_interactive_msg_info() diff --git a/daras_ai_v2/facebook_bots.py b/daras_ai_v2/facebook_bots.py index 2a22bbddd..abf33b8a0 100644 --- a/daras_ai_v2/facebook_bots.py +++ b/daras_ai_v2/facebook_bots.py @@ -102,12 +102,12 @@ def send_msg( buttons: list[ReplyButton] = None, documents: list[str] = None, should_translate: bool = False, + update_msg_id: str = None, ) -> str | None: if text and should_translate and self.language and self.language != "en": text = run_google_translate( [text], self.language, glossary_url=self.output_glossary )[0] - text = text or "\u200b" # handle empty text with zero-width space return self.send_msg_to( bot_number=self.bot_id, user_number=self.user_id, @@ -123,9 +123,9 @@ def mark_read(self): @classmethod def send_msg_to( - self, + cls, *, - text: str, + text: str = None, audio: str = None, video: str = None, documents: list[str] = None, @@ -137,7 +137,7 @@ def send_msg_to( # see https://developers.facebook.com/docs/whatsapp/api/messages/media/ # split text into chunks if too long - if len(text) > WA_MSG_MAX_SIZE: + if text and len(text) > WA_MSG_MAX_SIZE: splits = text_splitter( text, chunk_size=WA_MSG_MAX_SIZE, length_function=len ) @@ -160,6 +160,7 @@ def send_msg_to( ], ) + messages = [] if video: if buttons: messages = [ @@ -168,7 +169,7 @@ def send_msg_to( buttons, { "body": { - "text": text, + "text": text or "\u200b", }, "header": { "type": "video", @@ -188,74 +189,38 @@ def send_msg_to( }, }, ] - elif audio: - if buttons: - # audio can't be sent as an interaction, so send text and audio separately - messages = [ - # simple audio msg + elif buttons: + # interactive text msg + messages = [ + _build_msg_buttons( + buttons, { - "type": "audio", - "audio": {"link": audio}, + "body": { + "text": text or "\u200b", + } }, - ] - send_wa_msgs_raw( - bot_number=bot_number, - user_number=user_number, - messages=messages, - ) - messages = [ - # interactive text msg - _build_msg_buttons( - buttons, - { - "body": { - "text": text, - }, - }, - ) - ] - else: - # audio doesn't support captions, so send text and audio separately - messages = [ - # simple text msg - { - "type": "text", - "text": { - "body": text, - "preview_url": True, - }, - }, - # simple audio msg - { - "type": "audio", - "audio": {"link": audio}, - }, - ] - else: - # text message - if buttons: - messages = [ - # interactive text msg - _build_msg_buttons( - buttons, - { - "body": { - "text": text, - } - }, - ), - ] - else: - messages = [ - # simple text msg - { - "type": "text", - "text": { - "body": text, - "preview_url": True, - }, + ), + ] + elif text: + # simple text msg + messages = [ + { + "type": "text", + "text": { + "body": text, + "preview_url": True, }, - ] + }, + ] + + if audio and not video: # video already has audio + # simple audio msg + messages.append( + { + "type": "audio", + "audio": {"link": audio}, + } + ) if documents: messages += [ @@ -398,6 +363,7 @@ def send_msg( buttons: list[ReplyButton] = None, documents: list[str] = None, should_translate: bool = False, + update_msg_id: str = None, ) -> str | None: if text and should_translate and self.language and self.language != "en": text = run_google_translate( diff --git a/daras_ai_v2/language_model.py b/daras_ai_v2/language_model.py index 99452a193..bd6095590 100644 --- a/daras_ai_v2/language_model.py +++ b/daras_ai_v2/language_model.py @@ -208,12 +208,14 @@ def calc_gpt_tokens( for entry in messages if ( content := ( - format_chatml_message(entry) + "\n" - if is_chat_model - else entry.get("content", "") + ( + format_chatml_message(entry) + "\n" + if is_chat_model + else entry.get("content", "") + ) + if isinstance(entry, dict) + else str(entry) ) - if isinstance(entry, dict) - else str(entry) ) ) return default_length_function(combined) @@ -340,7 +342,7 @@ def run_language_model( ) -> ( list[str] | tuple[list[str], list[list[dict]]] - | typing.Generator[list[str], None, None] + | typing.Generator[list[dict], None, None] ): assert bool(prompt) != bool( messages @@ -376,7 +378,7 @@ def run_language_model( stream=stream and not (tools or response_format_type), ) if stream: - return _stream_llm_outputs(entries, is_chatml, response_format_type, tools) + return _stream_llm_outputs(entries, response_format_type) else: return _parse_entries(entries, is_chatml, response_format_type, tools) else: @@ -396,15 +398,28 @@ def run_language_model( ) ret = [msg.strip() for msg in msgs] if stream: - ret = [ret] + ret = [ + [ + format_chat_entry(role=CHATML_ROLE_ASSISTANT, content=msg) + for msg in ret + ] + ] return ret -def _stream_llm_outputs(result, is_chatml, response_format_type, tools): +def _stream_llm_outputs( + result: list | typing.Generator[list[ConversationEntry], None, None], + response_format_type: typing.Literal["text", "json_object"] | None, +): if isinstance(result, list): # compatibility with non-streaming apis result = [result] for entries in result: - yield _parse_entries(entries, is_chatml, response_format_type, tools) + if response_format_type == "json_object": + for i, entry in enumerate(entries): + entries[i] = json.loads(entry["content"]) + for i, entry in enumerate(entries): + entries[i]["content"] = entry.get("content") or "" + yield entries def _parse_entries( @@ -557,9 +572,11 @@ def _run_openai_chat( frequency_penalty=frequency_penalty, presence_penalty=presence_penalty, tools=[tool.spec for tool in tools] if tools else NOT_GIVEN, - response_format={"type": response_format_type} - if response_format_type - else NOT_GIVEN, + response_format=( + {"type": response_format_type} + if response_format_type + else NOT_GIVEN + ), stream=stream, ) for model_str in model @@ -576,24 +593,28 @@ def _stream_openai_chunked( start_chunk_size: int = 50, stop_chunk_size: int = 400, step_chunk_size: int = 150, -): +) -> typing.Generator[list[ConversationEntry], None, None]: ret = [] chunk_size = start_chunk_size for completion_chunk in r: changed = False for choice in completion_chunk.choices: + delta = choice.delta try: + # get the entry for this choice entry = ret[choice.index] except IndexError: # initialize the entry - entry = choice.delta.dict() | {"content": "", "chunk": ""} + entry = delta.dict() | {"content": "", "chunk": ""} ret.append(entry) + # this is to mark the end of streaming + entry["finish_reason"] = choice.finish_reason # append the delta to the current chunk - if not choice.delta.content: + if not delta.content: continue - entry["chunk"] += choice.delta.content + entry["chunk"] += delta.content # if the chunk is too small, we need to wait for more data chunk = entry["chunk"] if len(chunk) < chunk_size: diff --git a/daras_ai_v2/language_model_settings_widgets.py b/daras_ai_v2/language_model_settings_widgets.py index e5ab27a59..80f785bd4 100644 --- a/daras_ai_v2/language_model_settings_widgets.py +++ b/daras_ai_v2/language_model_settings_widgets.py @@ -24,9 +24,9 @@ def language_model_settings(show_selector=True, show_document_model=False): f"###### {field_title_desc(VideoBotsPage.RequestModel, 'document_model')}", key="document_model", options=[None, *doc_model_descriptions], - format_func=lambda x: f"{doc_model_descriptions[x]} ({x})" - if x - else "———", + format_func=lambda x: ( + f"{doc_model_descriptions[x]} ({x})" if x else "———" + ), ) st.checkbox("Avoid Repetition", key="avoid_repetition") diff --git a/daras_ai_v2/search_ref.py b/daras_ai_v2/search_ref.py index 7cc810b1a..e95fb2ab2 100644 --- a/daras_ai_v2/search_ref.py +++ b/daras_ai_v2/search_ref.py @@ -161,6 +161,8 @@ def format_citations( def format_footnotes( all_refs: dict[int, SearchReference], formatted: str, citation_style: CitationStyles ) -> str: + if not all_refs: + return formatted match citation_style: case CitationStyles.number_markdown: formatted += "\n\n" diff --git a/daras_ai_v2/slack_bot.py b/daras_ai_v2/slack_bot.py index f75863133..10c88f7d7 100644 --- a/daras_ai_v2/slack_bot.py +++ b/daras_ai_v2/slack_bot.py @@ -11,7 +11,7 @@ from bots.models import BotIntegration, Platform, Conversation from daras_ai.image_input import upload_file_from_bytes from daras_ai_v2.asr import run_google_translate, audio_bytes_to_wav -from daras_ai_v2.bots import BotInterface +from daras_ai_v2.bots import BotInterface, SLACK_MAX_SIZE from daras_ai_v2.exceptions import raise_for_status from daras_ai_v2.functional import fetch_parallel from daras_ai_v2.text_splitter import text_splitter @@ -27,11 +27,10 @@ I have been configured for $user_language and will respond to you in that language. """.strip() -SLACK_MAX_SIZE = 3000 - class SlackBot(BotInterface): platform = Platform.SLACK + can_update_message = True _read_rcpt_ts: str | None = None @@ -135,6 +134,7 @@ def send_msg( buttons: list[ReplyButton] = None, documents: list[str] = None, should_translate: bool = False, + update_msg_id: str | None = None, ) -> str | None: if text and should_translate and self.language and self.language != "en": text = run_google_translate( @@ -150,7 +150,9 @@ def send_msg( ) self._read_rcpt_ts = None - self._msg_ts = self.send_msg_to( + if not self.can_update_message: + update_msg_id = None + self._msg_ts, num_splits = self.send_msg_to( text=text, audio=audio, video=video, @@ -160,7 +162,10 @@ def send_msg( username=self.convo.bot_integration.name, token=self._access_token, thread_ts=self._msg_ts, + update_msg_ts=update_msg_id, ) + if num_splits > 1: + self.can_update_message = False return self._msg_ts @classmethod @@ -178,7 +183,8 @@ def send_msg_to( username: str, token: str, thread_ts: str = None, - ) -> str | None: + update_msg_ts: str = None, + ) -> tuple[str | None, int]: splits = text_splitter(text, chunk_size=SLACK_MAX_SIZE, length_function=len) for doc in splits[:-1]: thread_ts = chat_post_message( @@ -186,9 +192,11 @@ def send_msg_to( channel=channel, channel_is_personal=channel_is_personal, thread_ts=thread_ts, + update_msg_ts=update_msg_ts, username=username, token=token, ) + update_msg_ts = None thread_ts = chat_post_message( text=splits[-1].text, audio=audio, @@ -197,10 +205,11 @@ def send_msg_to( channel=channel, channel_is_personal=channel_is_personal, thread_ts=thread_ts, + update_msg_ts=update_msg_ts, username=username, token=token, ) - return thread_ts + return thread_ts, len(splits) def mark_read(self): text = self.convo.bot_integration.slack_read_receipt_msg.strip() @@ -524,9 +533,10 @@ def chat_post_message( channel: str, thread_ts: str, token: str, + update_msg_ts: str = None, channel_is_personal: bool = False, - audio: str | None = None, - video: str | None = None, + audio: str = None, + video: str = None, username: str = "Video Bot", buttons: list[ReplyButton] = None, ) -> str | None: @@ -535,28 +545,52 @@ def chat_post_message( if channel_is_personal: # don't thread in personal channels thread_ts = None - res = requests.post( - "https://slack.com/api/chat.postMessage", - json={ - "channel": channel, - "thread_ts": thread_ts, - "text": text, - "username": username, - "icon_emoji": ":robot_face:", - "blocks": [ - { - "type": "section", - "text": {"type": "mrkdwn", "text": text}, - }, - ] - + create_file_block("Audio", token, audio) - + create_file_block("Video", token, video) - + create_button_block(buttons), - }, - headers={ - "Authorization": f"Bearer {token}", - }, - ) + if update_msg_ts: + res = requests.post( + "https://slack.com/api/chat.update", + json={ + "channel": channel, + "ts": update_msg_ts, + "text": text, + "username": username, + "icon_emoji": ":robot_face:", + "blocks": [ + { + "type": "section", + "text": {"type": "mrkdwn", "text": text}, + }, + ] + + create_file_block("Audio", token, audio) + + create_file_block("Video", token, video) + + create_button_block(buttons), + }, + headers={ + "Authorization": f"Bearer {token}", + }, + ) + else: + res = requests.post( + "https://slack.com/api/chat.postMessage", + json={ + "channel": channel, + "thread_ts": thread_ts, + "text": text, + "username": username, + "icon_emoji": ":robot_face:", + "blocks": [ + { + "type": "section", + "text": {"type": "mrkdwn", "text": text}, + }, + ] + + create_file_block("Audio", token, audio) + + create_file_block("Video", token, video) + + create_button_block(buttons), + }, + headers={ + "Authorization": f"Bearer {token}", + }, + ) data = parse_slack_response(res) return data.get("ts") diff --git a/daras_ai_v2/stable_diffusion.py b/daras_ai_v2/stable_diffusion.py index 22c7adc8e..ce6333068 100644 --- a/daras_ai_v2/stable_diffusion.py +++ b/daras_ai_v2/stable_diffusion.py @@ -247,9 +247,9 @@ def instruct_pix2pix( }, inputs={ "prompt": [prompt] * len(images), - "negative_prompt": [negative_prompt] * len(images) - if negative_prompt - else None, + "negative_prompt": ( + [negative_prompt] * len(images) if negative_prompt else None + ), "num_images_per_prompt": num_outputs, "num_inference_steps": num_inference_steps, "guidance_scale": guidance_scale, @@ -440,9 +440,9 @@ def controlnet( pipeline={ "model_id": text2img_model_ids[Text2ImgModels[selected_model]], "seed": seed, - "scheduler": Schedulers[scheduler].label - if scheduler - else "UniPCMultistepScheduler", + "scheduler": ( + Schedulers[scheduler].label if scheduler else "UniPCMultistepScheduler" + ), "disable_safety_checker": True, "controlnet_model_id": [ controlnet_model_ids[ControlNetModels[model]] diff --git a/daras_ai_v2/vector_search.py b/daras_ai_v2/vector_search.py index 394d55b55..b2cc187d0 100644 --- a/daras_ai_v2/vector_search.py +++ b/daras_ai_v2/vector_search.py @@ -88,11 +88,11 @@ def get_top_k_references( Returns: the top k documents """ - yield "Checking docs..." + yield "Fetching latest knowledge docs..." input_docs = request.documents or [] doc_metas = map_parallel(doc_url_to_metadata, input_docs) - yield "Getting embeddings..." + yield "Creating knowledge embeddings..." embeds: list[tuple[SearchReference, np.ndarray]] = flatmap_parallel( lambda f_url, doc_meta: get_embeds_for_doc( f_url=f_url, @@ -107,7 +107,7 @@ def get_top_k_references( ) dense_query_embeds = openai_embedding_create([request.search_query])[0] - yield "Searching documents..." + yield "Searching knowledge base..." dense_weight = request.dense_weight if dense_weight is None: # for backwards compatibility @@ -133,7 +133,7 @@ def get_top_k_references( dense_ranks = np.zeros(len(embeds)) if sparse_weight: - yield "Getting sparse scores..." + yield "Considering results..." # get sparse scores bm25_corpus = flatmap_parallel( lambda f_url, doc_meta: get_bm25_embeds_for_doc( diff --git a/gooey_ui/pubsub.py b/gooey_ui/pubsub.py index 5e210c956..ef2ba1539 100644 --- a/gooey_ui/pubsub.py +++ b/gooey_ui/pubsub.py @@ -2,6 +2,7 @@ import json import threading import typing +from contextlib import contextmanager from time import time import redis @@ -49,6 +50,34 @@ def realtime_push(channel: str, value: typing.Any = "ping"): logger.info(f"publish {channel=}") +@contextmanager +def realtime_subscribe(channel: str) -> typing.Generator: + channel = f"gooey-gui/state/{channel}" + pubsub = r.pubsub() + pubsub.subscribe(channel) + logger.info(f"subscribe {channel=}") + try: + yield _realtime_sub_gen(channel, pubsub) + finally: + logger.info(f"unsubscribe {channel=}") + pubsub.unsubscribe(channel) + pubsub.close() + + +def _realtime_sub_gen(channel: str, pubsub: redis.client.PubSub) -> typing.Generator: + while True: + message = pubsub.get_message(timeout=10) + if not (message and message["type"] == "message"): + continue + value = json.loads(r.get(channel)) + if isinstance(value, dict): + run_status = value.get("__run_status") + logger.info(f"realtime_subscribe: {channel=} {run_status=}") + else: + logger.info(f"realtime_subscribe: {channel=}") + yield value + + # def use_state( # value: T = None, *, key: str = None # ) -> tuple[T, typing.Callable[[T], None]]: diff --git a/gooeysite/urls.py b/gooeysite/urls.py index 767660f21..6c3809436 100644 --- a/gooeysite/urls.py +++ b/gooeysite/urls.py @@ -14,6 +14,7 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ + from django.contrib import admin from django.urls import path diff --git a/poetry.lock b/poetry.lock index edbc668d1..a5c39450e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -13,13 +13,13 @@ files = [ [[package]] name = "aifail" -version = "0.1.0" +version = "0.2.0" description = "" optional = false python-versions = ">=3.10,<4.0" files = [ - {file = "aifail-0.1.0-py3-none-any.whl", hash = "sha256:d51fa56b1b8531a298a38cf91a3979f7d427afc88f5af635f86865b8f721bd23"}, - {file = "aifail-0.1.0.tar.gz", hash = "sha256:40f95fd45c07f9a7f0478d9702e3ea0d811f8582da7f6b841618de2a52803177"}, + {file = "aifail-0.2.0-py3-none-any.whl", hash = "sha256:83f3a842dbe523ee10a4d53ee00f06e794176122b93b57752566fb98d60db603"}, + {file = "aifail-0.2.0.tar.gz", hash = "sha256:d3e19e16740577181922055883a00e894d11413d12803e8d443094846040d6df"}, ] [package.dependencies] @@ -6510,4 +6510,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "a5ef77e11ff5b9bb9a5ab5ec5a07c2d3ffa0ef3204d332653d3039e8aba04e14" +content-hash = "a0d934b2a9b3b5c54d3b14ad5e524ff4b163eaa5eaeb794fe38bc3a8d8d60e04" diff --git a/pyproject.toml b/pyproject.toml index c6a699024..aac74b09c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,7 +81,7 @@ ua-parser = "^0.18.0" user-agents = "^2.2.0" openpyxl = "^3.1.2" loguru = "^0.7.2" -aifail = "^0.1.0" +aifail = "0.2.0" pytest-playwright = "^0.4.3" [tool.poetry.group.dev.dependencies] @@ -93,3 +93,6 @@ pre-commit = "^3.5.0" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" + +[tool.black] +force-exclude = "migrations" diff --git a/recipes/CompareLLM.py b/recipes/CompareLLM.py index c07bcffa6..2101aece5 100644 --- a/recipes/CompareLLM.py +++ b/recipes/CompareLLM.py @@ -36,9 +36,9 @@ class CompareLLMPage(BasePage): class RequestModel(BaseModel): input_prompt: str | None - selected_models: list[ - typing.Literal[tuple(e.name for e in LargeLanguageModels)] - ] | None + selected_models: ( + list[typing.Literal[tuple(e.name for e in LargeLanguageModels)]] | None + ) avoid_repetition: bool | None num_outputs: int | None @@ -106,8 +106,8 @@ def run(self, state: dict) -> typing.Iterator[str | None]: avoid_repetition=request.avoid_repetition, stream=True, ) - for i, item in enumerate(ret): - output_text[selected_model] = item + for i, entries in enumerate(ret): + output_text[selected_model] = [e["content"] for e in entries] yield f"Streaming{str(i + 1).translate(SUPERSCRIPT)} {model.value}..." def render_output(self): diff --git a/recipes/CompareText2Img.py b/recipes/CompareText2Img.py index e79ef5d54..5f44ca34b 100644 --- a/recipes/CompareText2Img.py +++ b/recipes/CompareText2Img.py @@ -64,9 +64,9 @@ class RequestModel(BaseModel): seed: int | None sd_2_upscaling: bool | None - selected_models: list[ - typing.Literal[tuple(e.name for e in Text2ImgModels)] - ] | None + selected_models: ( + list[typing.Literal[tuple(e.name for e in Text2ImgModels)]] | None + ) scheduler: typing.Literal[tuple(e.name for e in Schedulers)] | None edit_instruction: str | None diff --git a/recipes/CompareUpscaler.py b/recipes/CompareUpscaler.py index f6f4e988b..e4ed8865f 100644 --- a/recipes/CompareUpscaler.py +++ b/recipes/CompareUpscaler.py @@ -24,9 +24,9 @@ class RequestModel(BaseModel): scale: int - selected_models: list[ - typing.Literal[tuple(e.name for e in UpscalerModels)] - ] | None + selected_models: ( + list[typing.Literal[tuple(e.name for e in UpscalerModels)]] | None + ) class ResponseModel(BaseModel): output_images: dict[typing.Literal[tuple(e.name for e in UpscalerModels)], str] diff --git a/recipes/DocExtract.py b/recipes/DocExtract.py index 6c9489fff..2672fffdb 100644 --- a/recipes/DocExtract.py +++ b/recipes/DocExtract.py @@ -77,9 +77,9 @@ class RequestModel(BaseModel): task_instructions: str | None - selected_model: typing.Literal[ - tuple(e.name for e in LargeLanguageModels) - ] | None + selected_model: ( + typing.Literal[tuple(e.name for e in LargeLanguageModels)] | None + ) avoid_repetition: bool | None num_outputs: int | None quality: float | None diff --git a/recipes/DocSearch.py b/recipes/DocSearch.py index 15cb6b063..0b95464e1 100644 --- a/recipes/DocSearch.py +++ b/recipes/DocSearch.py @@ -61,9 +61,9 @@ class RequestModel(DocSearchRequest): task_instructions: str | None query_instructions: str | None - selected_model: typing.Literal[ - tuple(e.name for e in LargeLanguageModels) - ] | None + selected_model: ( + typing.Literal[tuple(e.name for e in LargeLanguageModels)] | None + ) avoid_repetition: bool | None num_outputs: int | None quality: float | None diff --git a/recipes/DocSummary.py b/recipes/DocSummary.py index 4b9283cde..498f2d405 100644 --- a/recipes/DocSummary.py +++ b/recipes/DocSummary.py @@ -60,9 +60,9 @@ class RequestModel(BaseModel): task_instructions: str | None merge_instructions: str | None - selected_model: typing.Literal[ - tuple(e.name for e in LargeLanguageModels) - ] | None + selected_model: ( + typing.Literal[tuple(e.name for e in LargeLanguageModels)] | None + ) avoid_repetition: bool | None num_outputs: int | None quality: float | None diff --git a/recipes/GoogleGPT.py b/recipes/GoogleGPT.py index 57b1078ba..7a2d2591f 100644 --- a/recipes/GoogleGPT.py +++ b/recipes/GoogleGPT.py @@ -79,9 +79,9 @@ class RequestModel(GoogleSearchMixin, BaseModel): task_instructions: str | None query_instructions: str | None - selected_model: typing.Literal[ - tuple(e.name for e in LargeLanguageModels) - ] | None + selected_model: ( + typing.Literal[tuple(e.name for e in LargeLanguageModels)] | None + ) avoid_repetition: bool | None num_outputs: int | None quality: float | None diff --git a/recipes/ImageSegmentation.py b/recipes/ImageSegmentation.py index 543664065..751a0936b 100644 --- a/recipes/ImageSegmentation.py +++ b/recipes/ImageSegmentation.py @@ -49,9 +49,9 @@ class ImageSegmentationPage(BasePage): class RequestModel(BaseModel): input_image: str - selected_model: typing.Literal[ - tuple(e.name for e in ImageSegmentationModels) - ] | None + selected_model: ( + typing.Literal[tuple(e.name for e in ImageSegmentationModels)] | None + ) mask_threshold: float | None rect_persepective_transform: bool | None diff --git a/recipes/Img2Img.py b/recipes/Img2Img.py index a97bc7c62..68e696c08 100644 --- a/recipes/Img2Img.py +++ b/recipes/Img2Img.py @@ -46,9 +46,11 @@ class RequestModel(BaseModel): text_prompt: str | None selected_model: typing.Literal[tuple(e.name for e in Img2ImgModels)] | None - selected_controlnet_model: list[ - typing.Literal[tuple(e.name for e in ControlNetModels)] - ] | typing.Literal[tuple(e.name for e in ControlNetModels)] | None + selected_controlnet_model: ( + list[typing.Literal[tuple(e.name for e in ControlNetModels)]] + | typing.Literal[tuple(e.name for e in ControlNetModels)] + | None + ) negative_prompt: str | None num_outputs: int | None diff --git a/recipes/QRCodeGenerator.py b/recipes/QRCodeGenerator.py index 6b7260368..34b615225 100644 --- a/recipes/QRCodeGenerator.py +++ b/recipes/QRCodeGenerator.py @@ -88,18 +88,18 @@ class RequestModel(BaseModel): text_prompt: str negative_prompt: str | None image_prompt: str | None - image_prompt_controlnet_models: list[ - typing.Literal[tuple(e.name for e in ControlNetModels)], ... - ] | None + image_prompt_controlnet_models: ( + list[typing.Literal[tuple(e.name for e in ControlNetModels)], ...] | None + ) image_prompt_strength: float | None image_prompt_scale: float | None image_prompt_pos_x: float | None image_prompt_pos_y: float | None selected_model: typing.Literal[tuple(e.name for e in Text2ImgModels)] | None - selected_controlnet_model: list[ - typing.Literal[tuple(e.name for e in ControlNetModels)], ... - ] | None + selected_controlnet_model: ( + list[typing.Literal[tuple(e.name for e in ControlNetModels)], ...] | None + ) output_width: int | None output_height: int | None diff --git a/recipes/SEOSummary.py b/recipes/SEOSummary.py index a82424ae1..78328d822 100644 --- a/recipes/SEOSummary.py +++ b/recipes/SEOSummary.py @@ -98,9 +98,9 @@ class RequestModel(GoogleSearchMixin, BaseModel): enable_html: bool | None - selected_model: typing.Literal[ - tuple(e.name for e in LargeLanguageModels) - ] | None + selected_model: ( + typing.Literal[tuple(e.name for e in LargeLanguageModels)] | None + ) sampling_temperature: float | None max_tokens: int | None num_outputs: int | None diff --git a/recipes/SmartGPT.py b/recipes/SmartGPT.py index 554a74940..0aabcfefb 100644 --- a/recipes/SmartGPT.py +++ b/recipes/SmartGPT.py @@ -34,9 +34,9 @@ class RequestModel(BaseModel): reflexion_prompt: str | None dera_prompt: str | None - selected_model: typing.Literal[ - tuple(e.name for e in LargeLanguageModels) - ] | None + selected_model: ( + typing.Literal[tuple(e.name for e in LargeLanguageModels)] | None + ) avoid_repetition: bool | None num_outputs: int | None quality: float | None diff --git a/recipes/SocialLookupEmail.py b/recipes/SocialLookupEmail.py index 082fdcdaa..1094cea53 100644 --- a/recipes/SocialLookupEmail.py +++ b/recipes/SocialLookupEmail.py @@ -40,9 +40,9 @@ class RequestModel(BaseModel): domain: str | None key_words: str | None - selected_model: typing.Literal[ - tuple(e.name for e in LargeLanguageModels) - ] | None + selected_model: ( + typing.Literal[tuple(e.name for e in LargeLanguageModels)] | None + ) sampling_temperature: float | None max_tokens: int | None diff --git a/recipes/Text2Audio.py b/recipes/Text2Audio.py index 77776ddf5..18270cc8c 100644 --- a/recipes/Text2Audio.py +++ b/recipes/Text2Audio.py @@ -49,9 +49,9 @@ class RequestModel(BaseModel): seed: int | None sd_2_upscaling: bool | None - selected_models: list[ - typing.Literal[tuple(e.name for e in Text2AudioModels)] - ] | None + selected_models: ( + list[typing.Literal[tuple(e.name for e in Text2AudioModels)]] | None + ) class ResponseModel(BaseModel): output_audios: dict[ @@ -114,9 +114,9 @@ def run(self, state: dict) -> typing.Iterator[str | None]: ), inputs=dict( prompt=[request.text_prompt], - negative_prompt=[request.negative_prompt] - if request.negative_prompt - else None, + negative_prompt=( + [request.negative_prompt] if request.negative_prompt else None + ), num_waveforms_per_prompt=request.num_outputs, num_inference_steps=request.quality, guidance_scale=request.guidance_scale, diff --git a/recipes/TextToSpeech.py b/recipes/TextToSpeech.py index c37a8eb5f..721b2cb7a 100644 --- a/recipes/TextToSpeech.py +++ b/recipes/TextToSpeech.py @@ -53,9 +53,9 @@ class TextToSpeechPage(BasePage): class RequestModel(BaseModel): text_prompt: str - tts_provider: typing.Literal[ - tuple(e.name for e in TextToSpeechProviders) - ] | None + tts_provider: ( + typing.Literal[tuple(e.name for e in TextToSpeechProviders)] | None + ) uberduck_voice_name: str | None uberduck_speaking_rate: float | None diff --git a/recipes/VideoBots.py b/recipes/VideoBots.py index 44994b7ff..5d23502ff 100644 --- a/recipes/VideoBots.py +++ b/recipes/VideoBots.py @@ -163,9 +163,9 @@ class RequestModel(BaseModel): messages: list[ConversationEntry] | None # tts settings - tts_provider: typing.Literal[ - tuple(e.name for e in TextToSpeechProviders) - ] | None + tts_provider: ( + typing.Literal[tuple(e.name for e in TextToSpeechProviders)] | None + ) uberduck_voice_name: str | None uberduck_speaking_rate: float | None google_voice_name: str | None @@ -180,9 +180,9 @@ class RequestModel(BaseModel): elevenlabs_similarity_boost: float | None # llm settings - selected_model: typing.Literal[ - tuple(e.name for e in LargeLanguageModels) - ] | None + selected_model: ( + typing.Literal[tuple(e.name for e in LargeLanguageModels)] | None + ) document_model: str | None = Field( title="🩻 Photo / Document Intelligence", description="When your copilot users upload a photo or pdf, what kind of document are they mostly likely to upload? " @@ -713,7 +713,7 @@ def run(self, state: dict) -> typing.Iterator[str | None]: query_instructions = (request.query_instructions or "").strip() if query_instructions: - yield "Generating search query..." + yield "Creating search query..." state["final_search_query"] = generate_final_search_query( request=request, instructions=query_instructions, @@ -727,7 +727,7 @@ def run(self, state: dict) -> typing.Iterator[str | None]: keyword_instructions = (request.keyword_instructions or "").strip() if keyword_instructions: - yield "Extracting keywords..." + yield "Finding keywords..." k_request = request.copy() # other models dont support JSON mode k_request.selected_model = LargeLanguageModels.gpt_4_turbo.name @@ -809,7 +809,7 @@ def run(self, state: dict) -> typing.Iterator[str | None]: if max_allowed_tokens < 0: raise ValueError("Input Script is too long! Please reduce the script size.") - yield f"Running {model.value}..." + yield f"Summarizing with {model.value}..." if is_chat_model: chunks = run_language_model( model=request.selected_model, @@ -842,15 +842,16 @@ def run(self, state: dict) -> typing.Iterator[str | None]: citation_style = ( request.citation_style and CitationStyles[request.citation_style] ) or None - all_refs_list = [] - for i, output_text in enumerate(chunks): + for i, entries in enumerate(chunks): + if not entries: + continue + output_text = [entry["content"] for entry in entries] if request.tools: - output_text, tool_call_choices = output_text + # output_text, tool_call_choices = output_text state["output_documents"] = output_documents = [] - for tool_calls in tool_call_choices: - for call in tool_calls: - result = yield from exec_tool_call(call) - output_documents.append(result) + for call in entries[0].get("tool_calls") or []: + result = yield from exec_tool_call(call) + output_documents.append(result) # save model response state["raw_output_text"] = [ @@ -876,13 +877,20 @@ def run(self, state: dict) -> typing.Iterator[str | None]: all_refs_list = apply_response_formattings_prefix( output_text, references, citation_style ) + else: + all_refs_list = None + state["output_text"] = output_text - yield f"Streaming{str(i + 1).translate(SUPERSCRIPT)} {model.value}..." + if all(entry.get("finish_reason") for entry in entries): + if all_refs_list: + apply_response_formattings_suffix( + all_refs_list, state["output_text"], citation_style + ) + finish_reason = entries[0]["finish_reason"] + yield f"Completed with {finish_reason=}" # avoid changing this message since it's used to detect end of stream + else: + yield f"Streaming{str(i + 1).translate(SUPERSCRIPT)} {model.value}..." - if all_refs_list: - apply_response_formattings_suffix( - all_refs_list, state["output_text"], citation_style - ) state["output_audio"] = [] state["output_video"] = [] @@ -1042,12 +1050,27 @@ def messenger_bot_integration(self): col1, col2, col3, *_ = st.columns([1, 1, 2]) with col1: favicon = Platform(bi.platform).get_favicon() + if bi.published_run: + url = self.app_url( + example_id=bi.published_run.published_run_id, + tab_name=MenuTabs.paths[MenuTabs.integrations], + ) + elif bi.saved_run: + url = self.app_url( + run_id=bi.saved_run.run_id, + uid=bi.saved_run.uid, + example_id=bi.saved_run.example_id, + tab_name=MenuTabs.paths[MenuTabs.integrations], + ) + else: + url = None + if url: + href = f'{bi}' + else: + href = f"{bi}" with st.div(className="mt-2"): st.markdown( - f'  ' - f'{bi}' - if bi.saved_run - else f"{bi}", + f'  {href}', unsafe_allow_html=True, ) with col2: @@ -1082,7 +1105,10 @@ def messenger_bot_integration(self): st.session_state.get("user_language") or bi.user_language ) bi.saved_run = current_run - bi.published_run = published_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 @@ -1098,9 +1124,9 @@ def slack_specific_settings(self, bi: BotIntegration): st.session_state[f"_bi_name_{bi.id}"] = ( pr and pr.title ) or self.get_recipe_title() - st.session_state[ - f"_bi_slack_read_receipt_msg_{bi.id}" - ] = BotIntegration._meta.get_field("slack_read_receipt_msg").default + st.session_state[f"_bi_slack_read_receipt_msg_{bi.id}"] = ( + BotIntegration._meta.get_field("slack_read_receipt_msg").default + ) bi.slack_read_receipt_msg = st.text_input( """ @@ -1121,6 +1147,7 @@ def slack_specific_settings(self, bi: BotIntegration): value=bi.name, key=f"_bi_name_{bi.id}", ) + st.caption("Enable streaming messages to Slack in real-time.") def show_landbot_widget(): @@ -1299,9 +1326,9 @@ def msg_container_widget(role: str): return st.div( className="px-3 py-1 pt-2", style=dict( - background="rgba(239, 239, 239, 0.6)" - if role == CHATML_ROLE_USER - else "#fff", + background=( + "rgba(239, 239, 239, 0.6)" if role == CHATML_ROLE_USER else "#fff" + ), ), ) diff --git a/recipes/VideoBotsStats.py b/recipes/VideoBotsStats.py index c94b847f2..00c7f07dd 100644 --- a/recipes/VideoBotsStats.py +++ b/recipes/VideoBotsStats.py @@ -1,3 +1,5 @@ +from django.utils import timezone + from daras_ai_v2.base import BasePage, MenuTabs import gooey_ui as st from furl import furl @@ -31,7 +33,7 @@ TruncYear, Concat, ) -from django.db.models import Count +from django.db.models import Count, Avg ID_COLUMNS = [ "conversation__fb_page_id", @@ -119,6 +121,7 @@ def render(self): if int(bid) not in allowed_bids: bid = allowed_bids[0] bi = BotIntegration.objects.get(id=bid) + has_analysis_run = bi.analysis_run is not None run_title, run_url = self.parse_run_info(bi) self.show_title_breadcrumb_share(run_title, run_url, bi) @@ -136,7 +139,7 @@ def render(self): view, factor, trunc_fn, - ) = self.render_date_view_inputs() + ) = self.render_date_view_inputs(bi) df = self.calculate_stats_binned_by_time( bi, start_date, end_date, factor, trunc_fn @@ -153,18 +156,26 @@ def render(self): st.session_state.setdefault("details", self.request.query_params.get("details")) details = st.horizontal_radio( "### Details", - options=[ - "All Conversations", - "All Messages", - "Feedback Positive", - "Feedback Negative", - "Answered Successfully", - "Answered Unsuccessfully", - ], + options=( + [ + "Conversations", + "Messages", + "Feedback Positive", + "Feedback Negative", + ] + + ( + [ + "Answered Successfully", + "Answered Unsuccessfully", + ] + if has_analysis_run + else [] + ) + ), key="details", ) - if details == "All Conversations": + if details == "Conversations": options = [ "Messages", "Correct Answers", @@ -210,7 +221,15 @@ def render(self): sort_by = st.session_state["sort_by"] df = self.get_tabular_data( - bi, run_url, conversations, messages, details, sort_by, rows=500 + bi, + run_url, + conversations, + messages, + details, + sort_by, + rows=500, + start_date=start_date, + end_date=end_date, ) if not df.empty: @@ -233,7 +252,14 @@ def render(self): st.html("
") if st.checkbox("Export"): df = self.get_tabular_data( - bi, run_url, conversations, messages, details, sort_by + bi, + run_url, + conversations, + messages, + details, + sort_by, + start_date=start_date, + end_date=end_date, ) csv = df.to_csv() b64 = base64.b64encode(csv.encode()).decode() @@ -260,27 +286,34 @@ def render(self): ).tostr() st.change_url(new_url, self.request) - def render_date_view_inputs(self): - start_of_year_date = datetime.now().replace(month=1, day=1) - st.session_state.setdefault( - "start_date", - self.request.query_params.get( - "start_date", start_of_year_date.strftime("%Y-%m-%d") - ), - ) - start_date: datetime = ( - st.date_input("Start date", key="start_date") or start_of_year_date - ) - st.session_state.setdefault( - "end_date", - self.request.query_params.get( - "end_date", datetime.now().strftime("%Y-%m-%d") - ), - ) - end_date: datetime = st.date_input("End date", key="end_date") or datetime.now() - st.session_state.setdefault( - "view", self.request.query_params.get("view", "Weekly") - ) + def render_date_view_inputs(self, bi): + if st.checkbox("Show All"): + start_date = bi.created_at + end_date = timezone.now() + else: + start_of_year_date = timezone.now().replace(month=1, day=1) + st.session_state.setdefault( + "start_date", + self.request.query_params.get( + "start_date", start_of_year_date.strftime("%Y-%m-%d") + ), + ) + start_date: datetime = ( + st.date_input("Start date", key="start_date") or start_of_year_date + ) + st.session_state.setdefault( + "end_date", + self.request.query_params.get( + "end_date", timezone.now().strftime("%Y-%m-%d") + ), + ) + end_date: datetime = ( + st.date_input("End date", key="end_date") or timezone.now() + ) + st.session_state.setdefault( + "view", self.request.query_params.get("view", "Weekly") + ) + st.write("---") view = st.horizontal_radio( "### View", options=["Daily", "Weekly", "Monthly"], @@ -307,15 +340,12 @@ def render_date_view_inputs(self): def parse_run_info(self, bi): saved_run = bi.get_active_saved_run() - run_title = ( - bi.published_run.title - if bi.published_run - else saved_run.page_title - if saved_run and saved_run.page_title - else "This Copilot Run" - if saved_run - else "No Run Connected" - ) + if bi.published_run: + run_title = bi.published_run.title + elif saved_run: + run_title = "This Copilot Run" + else: + run_title = "No Run Connected" run_url = furl(saved_run.get_app_url()).tostr() if saved_run else "" return run_title, run_url @@ -331,7 +361,7 @@ def calculate_overall_stats(self, bid, bi, run_title, run_url): num_active_users_last_7_days = ( user_messages.filter( conversation__in=users, - created_at__gte=datetime.now() - timedelta(days=7), + created_at__gte=timezone.now() - timedelta(days=7), ) .distinct( *ID_COLUMNS, @@ -341,7 +371,7 @@ def calculate_overall_stats(self, bid, bi, run_title, run_url): num_active_users_last_30_days = ( user_messages.filter( conversation__in=users, - created_at__gte=datetime.now() - timedelta(days=30), + created_at__gte=timezone.now() - timedelta(days=30), ) .distinct( *ID_COLUMNS, @@ -393,7 +423,7 @@ def calculate_stats_binned_by_time( created_at__date__gte=start_date, created_at__date__lte=end_date, conversation__bot_integration=bi, - role=CHATML_ROLE_USER, + role=CHATML_ROLE_ASSISTANT, ) .order_by() .annotate(date=trunc_fn("created_at")) @@ -408,6 +438,9 @@ def calculate_stats_binned_by_time( distinct=True, ) ) + .annotate(Average_runtime=Avg("saved_run__run_time")) + .annotate(Average_response_time=Avg("response_time")) + .annotate(Average_analysis_time=Avg("analysis_run__run_time")) .annotate(Unique_feedback_givers=Count("feedbacks", distinct=True)) .values( "date", @@ -415,6 +448,9 @@ def calculate_stats_binned_by_time( "Convos", "Senders", "Unique_feedback_givers", + "Average_response_time", + "Average_runtime", + "Average_analysis_time", ) ) @@ -446,6 +482,20 @@ def calculate_stats_binned_by_time( .values("date", "Neg_feedback") ) + successfully_answered = ( + Message.objects.filter( + conversation__bot_integration=bi, + analysis_result__contains={"Answered": True}, + created_at__date__gte=start_date, + created_at__date__lte=end_date, + ) + .order_by() + .annotate(date=trunc_fn("created_at")) + .values("date") + .annotate(Successfully_Answered=Count("id")) + .values("date", "Successfully_Answered") + ) + df = pd.DataFrame( messages_received, columns=[ @@ -454,6 +504,9 @@ def calculate_stats_binned_by_time( "Convos", "Senders", "Unique_feedback_givers", + "Average_response_time", + "Average_runtime", + "Average_analysis_time", ], ) df = df.merge( @@ -468,14 +521,36 @@ def calculate_stats_binned_by_time( left_on="date", right_on="date", ) + df = df.merge( + pd.DataFrame( + successfully_answered, columns=["date", "Successfully_Answered"] + ), + how="outer", + left_on="date", + right_on="date", + ) df["Messages_Sent"] = df["Messages_Sent"] * factor df["Convos"] = df["Convos"] * factor df["Senders"] = df["Senders"] * factor df["Unique_feedback_givers"] = df["Unique_feedback_givers"] * factor df["Pos_feedback"] = df["Pos_feedback"] * factor df["Neg_feedback"] = df["Neg_feedback"] * factor + df["Percentage_positive_feedback"] = ( + df["Pos_feedback"] / df["Messages_Sent"] + ) * 100 + df["Percentage_negative_feedback"] = ( + df["Neg_feedback"] / df["Messages_Sent"] + ) * 100 + df["Percentage_successfully_answered"] = ( + df["Successfully_Answered"] / df["Messages_Sent"] + ) * 100 df["Msgs_per_convo"] = df["Messages_Sent"] / df["Convos"] df["Msgs_per_user"] = df["Messages_Sent"] / df["Senders"] + try: + df["Average_response_time"] = df["Average_response_time"].dt.total_seconds() + except AttributeError: + pass + df["Average_response_time"] = df["Average_response_time"] * factor df.fillna(0, inplace=True) df = df.round(0).astype("int32", errors="ignore") return df @@ -621,14 +696,121 @@ def plot_graphs(self, view, df): ], ) st.plotly_chart(fig) + st.write("---") + fig = go.Figure( + data=[ + go.Scatter( + name="Average Response Time", + mode="lines+markers", + x=list(df["date"]), + y=list(df["Average_response_time"]), + text=list(df["Average_response_time"]), + hovertemplate="Average Response Time: %{y:.0f}", + ), + go.Scatter( + name="Average Run Time", + mode="lines+markers", + x=list(df["date"]), + y=list(df["Average_runtime"]), + text=list(df["Average_runtime"]), + hovertemplate="Average Runtime: %{y:.0f}", + ), + go.Scatter( + name="Average Analysis Time", + mode="lines+markers", + x=list(df["date"]), + y=list(df["Average_analysis_time"]), + text=list(df["Average_analysis_time"]), + hovertemplate="Average Analysis Time: %{y:.0f}", + ), + ], + layout=dict( + margin=dict(l=0, r=0, t=28, b=0), + yaxis=dict( + title="Seconds", + ), + title=dict( + text=f"{view} Performance Metrics", + ), + height=300, + template="plotly_white", + ), + ) + st.plotly_chart(fig) + st.write("---") + fig = go.Figure( + data=[ + go.Scatter( + name="Positive Feedback", + mode="lines+markers", + x=list(df["date"]), + y=list(df["Percentage_positive_feedback"]), + text=list(df["Percentage_positive_feedback"]), + hovertemplate="Positive Feedback: %{y:.0f}%", + ), + go.Scatter( + name="Negative Feedback", + mode="lines+markers", + x=list(df["date"]), + y=list(df["Percentage_negative_feedback"]), + text=list(df["Percentage_negative_feedback"]), + hovertemplate="Negative Feedback: %{y:.0f}%", + ), + go.Scatter( + name="Successfully Answered", + mode="lines+markers", + x=list(df["date"]), + y=list(df["Percentage_successfully_answered"]), + text=list(df["Percentage_successfully_answered"]), + hovertemplate="Successfully Answered: %{y:.0f}%", + ), + ], + layout=dict( + margin=dict(l=0, r=0, t=28, b=0), + yaxis=dict( + title="Percentage", + range=[0, 100], + tickvals=[ + *range( + 0, + 101, + 10, + ) + ], + ), + title=dict( + text=f"{view} Feedback Distribution", + ), + height=300, + template="plotly_white", + ), + ) + st.plotly_chart(fig) def get_tabular_data( - self, bi, run_url, conversations, messages, details, sort_by, rows=10000 + self, + bi, + run_url, + conversations, + messages, + details, + sort_by, + rows=10000, + start_date=None, + end_date=None, ): df = pd.DataFrame() - if details == "All Conversations": + if details == "Conversations": + if start_date and end_date: + conversations = conversations.filter( + created_at__date__gte=start_date, created_at__date__lte=end_date + ) df = conversations.to_df_format(row_limit=rows) - elif details == "All Messages": + elif details == "Messages": + if start_date and end_date: + messages = messages.filter( + created_at__date__gte=start_date, created_at__date__lte=end_date + ) df = messages.order_by("-created_at", "conversation__id").to_df_format( row_limit=rows ) @@ -639,6 +821,10 @@ def get_tabular_data( message__conversation__bot_integration=bi, rating=Feedback.Rating.RATING_THUMBS_UP, ) # type: ignore + if start_date and end_date: + pos_feedbacks = pos_feedbacks.filter( + created_at__date__gte=start_date, created_at__date__lte=end_date + ) df = pos_feedbacks.to_df_format(row_limit=rows) df["Run URL"] = run_url df["Bot"] = bi.name @@ -646,7 +832,13 @@ def get_tabular_data( neg_feedbacks: FeedbackQuerySet = Feedback.objects.filter( message__conversation__bot_integration=bi, rating=Feedback.Rating.RATING_THUMBS_DOWN, + created_at__date__gte=start_date, + created_at__date__lte=end_date, ) # type: ignore + if start_date and end_date: + neg_feedbacks = neg_feedbacks.filter( + created_at__date__gte=start_date, created_at__date__lte=end_date + ) df = neg_feedbacks.to_df_format(row_limit=rows) df["Run URL"] = run_url df["Bot"] = bi.name @@ -654,7 +846,13 @@ def get_tabular_data( successful_messages: MessageQuerySet = Message.objects.filter( conversation__bot_integration=bi, analysis_result__contains={"Answered": True}, + created_at__date__gte=start_date, + created_at__date__lte=end_date, ) # type: ignore + if start_date and end_date: + successful_messages = successful_messages.filter( + created_at__date__gte=start_date, created_at__date__lte=end_date + ) df = successful_messages.to_df_analysis_format(row_limit=rows) df["Run URL"] = run_url df["Bot"] = bi.name @@ -662,12 +860,18 @@ def get_tabular_data( unsuccessful_messages: MessageQuerySet = Message.objects.filter( conversation__bot_integration=bi, analysis_result__contains={"Answered": False}, + created_at__date__gte=start_date, + created_at__date__lte=end_date, ) # type: ignore + if start_date and end_date: + unsuccessful_messages = unsuccessful_messages.filter( + created_at__date__gte=start_date, created_at__date__lte=end_date + ) df = unsuccessful_messages.to_df_analysis_format(row_limit=rows) df["Run URL"] = run_url df["Bot"] = bi.name - if sort_by: + if sort_by and sort_by in df.columns: df.sort_values(by=[sort_by], ascending=False, inplace=True) return df diff --git a/routers/api.py b/routers/api.py index c57d948a6..e58ae3b2b 100644 --- a/routers/api.py +++ b/routers/api.py @@ -367,7 +367,7 @@ def build_api_response( run_async: bool, created_at: str, ): - web_url = str(furl(self.app_url(run_id=run_id, uid=uid))) + web_url = self.app_url(run_id=run_id, uid=uid) if run_async: status_url = str( furl(settings.API_BASE_URL, query_params=dict(run_id=run_id)) diff --git a/scripts/test_wa_msg_send.py b/scripts/test_wa_msg_send.py new file mode 100644 index 000000000..c31fca0af --- /dev/null +++ b/scripts/test_wa_msg_send.py @@ -0,0 +1,124 @@ +from time import sleep + +from daras_ai_v2.bots import _feedback_start_buttons +from daras_ai_v2.facebook_bots import WhatsappBot + + +def run(bot_number: str, user_number: str): + WhatsappBot.send_msg_to( + bot_number=bot_number, + user_number=user_number, + text="", + buttons=_feedback_start_buttons(), + ) + sleep(1) + WhatsappBot.send_msg_to( + bot_number=bot_number, + user_number=user_number, + text="", + ) + sleep(1) + WhatsappBot.send_msg_to( + bot_number=bot_number, + user_number=user_number, + text="Text With Buttons", + buttons=_feedback_start_buttons(), + ) + sleep(1) + WhatsappBot.send_msg_to( + bot_number=bot_number, + user_number=user_number, + audio="https://storage.googleapis.com/dara-c1b52.appspot.com/daras_ai/media/d949d330-95cb-11ee-9a21-02420a00012e/google_tts_gen.mp3", + ) + sleep(1) + WhatsappBot.send_msg_to( + bot_number=bot_number, + user_number=user_number, + audio="https://storage.googleapis.com/dara-c1b52.appspot.com/daras_ai/media/d949d330-95cb-11ee-9a21-02420a00012e/google_tts_gen.mp3", + buttons=_feedback_start_buttons(), + ) + sleep(1) + WhatsappBot.send_msg_to( + bot_number=bot_number, + user_number=user_number, + text="Audio with text", + audio="https://storage.googleapis.com/dara-c1b52.appspot.com/daras_ai/media/d949d330-95cb-11ee-9a21-02420a00012e/google_tts_gen.mp3", + buttons=_feedback_start_buttons(), + ) + sleep(1) + WhatsappBot.send_msg_to( + bot_number=bot_number, + user_number=user_number, + text="Audio + Video", + audio="https://storage.googleapis.com/dara-c1b52.appspot.com/daras_ai/media/d949d330-95cb-11ee-9a21-02420a00012e/google_tts_gen.mp3", + video="https://storage.googleapis.com/dara-c1b52.appspot.com/daras_ai/media/6f019f2a-b714-11ee-82f3-02420a000172/gooey.ai%20lipsync.mp4#t=0.001", + buttons=_feedback_start_buttons(), + ) + sleep(1) + WhatsappBot.send_msg_to( + bot_number=bot_number, + user_number=user_number, + video="https://storage.googleapis.com/dara-c1b52.appspot.com/daras_ai/media/6f019f2a-b714-11ee-82f3-02420a000172/gooey.ai%20lipsync.mp4#t=0.001", + ) + sleep(1) + WhatsappBot.send_msg_to( + bot_number=bot_number, + user_number=user_number, + video="https://storage.googleapis.com/dara-c1b52.appspot.com/daras_ai/media/6f019f2a-b714-11ee-82f3-02420a000172/gooey.ai%20lipsync.mp4#t=0.001", + buttons=_feedback_start_buttons(), + ) + sleep(1) + WhatsappBot.send_msg_to( + bot_number=bot_number, + user_number=user_number, + text="Video with text", + video="https://storage.googleapis.com/dara-c1b52.appspot.com/daras_ai/media/6f019f2a-b714-11ee-82f3-02420a000172/gooey.ai%20lipsync.mp4#t=0.001", + buttons=_feedback_start_buttons(), + ) + sleep(1) + WhatsappBot.send_msg_to( + bot_number=bot_number, + user_number=user_number, + documents=[ + "https://storage.googleapis.com/dara-c1b52.appspot.com/daras_ai/media/d30155b8-b438-11ed-85e8-02420a0001f7/Chetan%20Bhagat%20-three%20mistakes%20of%20my%20life.pdf" + ], + ) + sleep(1) + WhatsappBot.send_msg_to( + bot_number=bot_number, + user_number=user_number, + documents=[ + "https://storage.googleapis.com/dara-c1b52.appspot.com/daras_ai/media/d30155b8-b438-11ed-85e8-02420a0001f7/Chetan%20Bhagat%20-three%20mistakes%20of%20my%20life.pdf" + ], + buttons=_feedback_start_buttons(), + ) + sleep(1) + WhatsappBot.send_msg_to( + bot_number=bot_number, + user_number=user_number, + text="Some Docs", + documents=[ + "https://storage.googleapis.com/dara-c1b52.appspot.com/daras_ai/media/d30155b8-b438-11ed-85e8-02420a0001f7/Chetan%20Bhagat%20-three%20mistakes%20of%20my%20life.pdf" + ], + ) + sleep(1) + WhatsappBot.send_msg_to( + bot_number=bot_number, + user_number=user_number, + text="Some Docs", + documents=[ + "https://storage.googleapis.com/dara-c1b52.appspot.com/daras_ai/media/d30155b8-b438-11ed-85e8-02420a0001f7/Chetan%20Bhagat%20-three%20mistakes%20of%20my%20life.pdf" + ], + buttons=_feedback_start_buttons(), + ) + sleep(1) + WhatsappBot.send_msg_to( + bot_number=bot_number, + user_number=user_number, + text="Audio + Docs", + audio="https://storage.googleapis.com/dara-c1b52.appspot.com/daras_ai/media/d949d330-95cb-11ee-9a21-02420a00012e/google_tts_gen.mp3", + documents=[ + "https://storage.googleapis.com/dara-c1b52.appspot.com/daras_ai/media/d30155b8-b438-11ed-85e8-02420a0001f7/Chetan%20Bhagat%20-three%20mistakes%20of%20my%20life.pdf" + ], + buttons=_feedback_start_buttons(), + )