From ff3c86f98cd70f7dd44174cfb71013d0406014b5 Mon Sep 17 00:00:00 2001 From: Alexander Metzger Date: Thu, 25 Jan 2024 12:02:58 -0800 Subject: [PATCH 01/23] fixes keyerror with sorting options --- bots/models.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/bots/models.py b/bots/models.py index 3ad94cd31..30c294f3e 100644 --- a/bots/models.py +++ b/bots/models.py @@ -704,7 +704,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 @@ -900,7 +919,17 @@ def to_df_format( "Analysis JSON": message.analysis_result, } rows.append(row) - df = pd.DataFrame.from_records(rows) + df = pd.DataFrame.from_records( + rows, + columns=[ + "Name", + "Role", + "Message (EN)", + "Sent", + "Feedback", + "Analysis JSON", + ], + ) return df def to_df_analysis_format( @@ -922,7 +951,10 @@ def to_df_analysis_format( "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"]: @@ -1107,7 +1139,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 From 4733be17e0d0cd9620cc45a043bea471841699ac Mon Sep 17 00:00:00 2001 From: Alexander Metzger Date: Thu, 25 Jan 2024 12:07:43 -0800 Subject: [PATCH 02/23] make 100% sure sortby is valid --- recipes/VideoBotsStats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recipes/VideoBotsStats.py b/recipes/VideoBotsStats.py index c94b847f2..b57c9f2ac 100644 --- a/recipes/VideoBotsStats.py +++ b/recipes/VideoBotsStats.py @@ -667,7 +667,7 @@ def get_tabular_data( 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 From dcc057a0e545306a1afc5bc7089e0baf125f416f Mon Sep 17 00:00:00 2001 From: Alexander Metzger Date: Thu, 25 Jan 2024 12:45:19 -0800 Subject: [PATCH 03/23] added regression test --- bots/models.py | 6 ++--- bots/tests.py | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/bots/models.py b/bots/models.py index 30c294f3e..457f3bb6a 100644 --- a/bots/models.py +++ b/bots/models.py @@ -937,14 +937,14 @@ 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"), 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 From ffb0a619efb118c77335d1bfad6a99fc9e6a275b Mon Sep 17 00:00:00 2001 From: Alexander Metzger Date: Mon, 29 Jan 2024 01:49:09 -0800 Subject: [PATCH 04/23] rename tables to remove "all" --- recipes/VideoBotsStats.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/recipes/VideoBotsStats.py b/recipes/VideoBotsStats.py index b57c9f2ac..e625fc977 100644 --- a/recipes/VideoBotsStats.py +++ b/recipes/VideoBotsStats.py @@ -154,8 +154,8 @@ def render(self): details = st.horizontal_radio( "### Details", options=[ - "All Conversations", - "All Messages", + "Conversations", + "Messages", "Feedback Positive", "Feedback Negative", "Answered Successfully", @@ -164,7 +164,7 @@ def render(self): key="details", ) - if details == "All Conversations": + if details == "Conversations": options = [ "Messages", "Correct Answers", @@ -310,11 +310,11 @@ def parse_run_info(self, bi): 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" + else ( + saved_run.page_title + if saved_run and saved_run.page_title + else "This Copilot Run" if saved_run else "No Run Connected" + ) ) run_url = furl(saved_run.get_app_url()).tostr() if saved_run else "" return run_title, run_url @@ -626,9 +626,9 @@ def get_tabular_data( self, bi, run_url, conversations, messages, details, sort_by, rows=10000 ): df = pd.DataFrame() - if details == "All Conversations": + if details == "Conversations": df = conversations.to_df_format(row_limit=rows) - elif details == "All Messages": + elif details == "Messages": df = messages.order_by("-created_at", "conversation__id").to_df_format( row_limit=rows ) From d0b28f25283c590d5ea6d4c1c27c6ab94871b15b Mon Sep 17 00:00:00 2001 From: Alexander Metzger Date: Mon, 29 Jan 2024 03:42:21 -0800 Subject: [PATCH 05/23] average response time --- recipes/VideoBotsStats.py | 45 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/recipes/VideoBotsStats.py b/recipes/VideoBotsStats.py index e625fc977..4939944cd 100644 --- a/recipes/VideoBotsStats.py +++ b/recipes/VideoBotsStats.py @@ -31,7 +31,8 @@ TruncYear, Concat, ) -from django.db.models import Count +from django.db.models import Count, F, Window +from django.db.models.functions import Lag ID_COLUMNS = [ "conversation__fb_page_id", @@ -388,6 +389,33 @@ def calculate_overall_stats(self, bid, bi, run_title, run_url): def calculate_stats_binned_by_time( self, bi, start_date, end_date, factor, trunc_fn ): + average_response_time = ( + Message.objects.filter( + created_at__date__gte=start_date, + created_at__date__lte=end_date, + conversation__bot_integration=bi, + ) + .values("conversation_id") + .order_by("created_at") + .annotate( + response_time=F("created_at") - Window(expression=Lag("created_at")), + ) + .annotate(date=trunc_fn("created_at")) + .values("date", "response_time", "role") + ) + average_response_time = ( + pd.DataFrame( + average_response_time, + columns=["date", "response_time", "role"], + ) + .loc[lambda df: df["role"] == CHATML_ROLE_ASSISTANT] + .groupby("date") + .agg({"response_time": "median"}) + .apply(lambda x: x.clip(lower=timedelta(0))) + .rename(columns={"response_time": "Average_response_time"}) + .reset_index() + ) + messages_received = ( Message.objects.filter( created_at__date__gte=start_date, @@ -468,6 +496,12 @@ def calculate_stats_binned_by_time( left_on="date", right_on="date", ) + df = df.merge( + average_response_time, + 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 @@ -476,6 +510,7 @@ def calculate_stats_binned_by_time( df["Neg_feedback"] = df["Neg_feedback"] * factor df["Msgs_per_convo"] = df["Messages_Sent"] / df["Convos"] df["Msgs_per_user"] = df["Messages_Sent"] / df["Senders"] + df["Average_response_time"] = df["Average_response_time"] * factor df.fillna(0, inplace=True) df = df.round(0).astype("int32", errors="ignore") return df @@ -576,6 +611,14 @@ def plot_graphs(self, view, df): text=list(df["Msgs_per_user"]), hovertemplate="Messages per User: %{y:.0f}", ), + 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}", + ), ], layout=dict( margin=dict(l=0, r=0, t=28, b=0), From b359f65adf93f73ac359c7e4683ccea1a36426f8 Mon Sep 17 00:00:00 2001 From: Alexander Metzger Date: Mon, 29 Jan 2024 03:42:26 -0800 Subject: [PATCH 06/23] linting --- bots/models.py | 12 +++++-- daras_ai_v2/bot_integration_widgets.py | 6 ++-- daras_ai_v2/bots.py | 12 ++++--- daras_ai_v2/language_model.py | 28 +++++++++------ .../language_model_settings_widgets.py | 6 ++-- daras_ai_v2/stable_diffusion.py | 12 +++---- gooeysite/urls.py | 1 + pyproject.toml | 3 ++ recipes/CompareLLM.py | 6 ++-- recipes/CompareText2Img.py | 6 ++-- recipes/CompareUpscaler.py | 6 ++-- recipes/DocExtract.py | 6 ++-- recipes/DocSearch.py | 6 ++-- recipes/DocSummary.py | 6 ++-- recipes/GoogleGPT.py | 6 ++-- recipes/ImageSegmentation.py | 6 ++-- recipes/Img2Img.py | 8 +++-- recipes/QRCodeGenerator.py | 12 +++---- recipes/SEOSummary.py | 6 ++-- recipes/SmartGPT.py | 6 ++-- recipes/SocialLookupEmail.py | 6 ++-- recipes/Text2Audio.py | 12 +++---- recipes/TextToSpeech.py | 6 ++-- recipes/VideoBots.py | 34 ++++++++++--------- 24 files changed, 120 insertions(+), 98 deletions(-) diff --git a/bots/models.py b/bots/models.py index 457f3bb6a..5148a531e 100644 --- a/bots/models.py +++ b/bots/models.py @@ -913,9 +913,11 @@ 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, } rows.append(row) @@ -1051,6 +1053,10 @@ def __str__(self): def local_lang(self): return Truncator(self.display_content).words(30) + @property + def response_time(self): + return self.created_at - self.get_previous_by_created_at().created_at + class MessageAttachment(models.Model): message = models.ForeignKey( diff --git a/daras_ai_v2/bot_integration_widgets.py b/daras_ai_v2/bot_integration_widgets.py index 3880f9fe7..2686b2298 100644 --- a/daras_ai_v2/bot_integration_widgets.py +++ b/daras_ai_v2/bot_integration_widgets.py @@ -19,9 +19,9 @@ 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.show_feedback_buttons = st.checkbox( diff --git a/daras_ai_v2/bots.py b/daras_ai_v2/bots.py index dfc00c794..8cc1d69e1 100644 --- a/daras_ai_v2/bots.py +++ b/daras_ai_v2/bots.py @@ -389,11 +389,13 @@ 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 + ), ) attachments = [] for f_url in (input_images or []) + (input_documents or []): diff --git a/daras_ai_v2/language_model.py b/daras_ai_v2/language_model.py index 80136cbe8..070aa1c22 100644 --- a/daras_ai_v2/language_model.py +++ b/daras_ai_v2/language_model.py @@ -198,12 +198,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) @@ -364,9 +366,11 @@ def run_language_model( else: out_content = [ # return messages back as either chatml or json messages - format_chatml_message(entry) - if is_chatml - else (entry.get("content") or "").strip() + ( + format_chatml_message(entry) + if is_chatml + else (entry.get("content") or "").strip() + ) for entry in result ] if tools: @@ -514,9 +518,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 + ), ) for model_str in model ], 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/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/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/pyproject.toml b/pyproject.toml index c6a699024..01ccdd04e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 583317ddc..f1be5a937 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 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 2a509d8fd..d18b5a54c 100644 --- a/recipes/DocSearch.py +++ b/recipes/DocSearch.py @@ -60,9 +60,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 149931ffb..faa88d533 100644 --- a/recipes/VideoBots.py +++ b/recipes/VideoBots.py @@ -157,9 +157,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 @@ -174,9 +174,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? " @@ -1028,10 +1028,12 @@ def messenger_bot_integration(self): favicon = Platform(bi.platform).get_favicon() with st.div(className="mt-2"): st.markdown( - f'  ' - f'{bi}' - if bi.saved_run - else f"{bi}", + ( + f'  ' + f'{bi}' + if bi.saved_run + else f"{bi}" + ), unsafe_allow_html=True, ) with col2: @@ -1082,9 +1084,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( """ @@ -1283,9 +1285,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" + ), ), ) From e00411135e7aeaaf5b9897c3ef1e614a1b174dd4 Mon Sep 17 00:00:00 2001 From: Alexander Metzger Date: Mon, 29 Jan 2024 03:51:42 -0800 Subject: [PATCH 07/23] add response time to table --- bots/models.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/bots/models.py b/bots/models.py index 5148a531e..61c56abc3 100644 --- a/bots/models.py +++ b/bots/models.py @@ -919,6 +919,7 @@ def to_df_format( else None ), # only show first feedback as per Sean's request "Analysis JSON": message.analysis_result, + "Response Time": message.response_time.total_seconds(), } rows.append(row) df = pd.DataFrame.from_records( @@ -930,6 +931,7 @@ def to_df_format( "Sent", "Feedback", "Analysis JSON", + "Response Time", ], ) return df @@ -1055,7 +1057,20 @@ def local_lang(self): @property def response_time(self): - return self.created_at - self.get_previous_by_created_at().created_at + import pandas as pd + + if self.role == CHATML_ROLE_USER: + return pd.NaT + return ( + self.created_at + - Message.objects.filter( + conversation=self.conversation, + role=CHATML_ROLE_USER, + created_at__lt=self.created_at, + ) + .latest() + .created_at + ) class MessageAttachment(models.Model): From 91e7810a8803c500a152d694c5687ff4d37f8e34 Mon Sep 17 00:00:00 2001 From: Alexander Metzger Date: Thu, 1 Feb 2024 12:07:40 -0800 Subject: [PATCH 08/23] SavedRun run_time attempt --- bots/models.py | 29 +++++++++++++++-------------- recipes/VideoBotsStats.py | 39 ++++----------------------------------- 2 files changed, 19 insertions(+), 49 deletions(-) diff --git a/bots/models.py b/bots/models.py index 0714f187b..204a625bd 100644 --- a/bots/models.py +++ b/bots/models.py @@ -1065,20 +1065,21 @@ def local_lang(self): @property def response_time(self): - import pandas as pd - - if self.role == CHATML_ROLE_USER: - return pd.NaT - return ( - self.created_at - - Message.objects.filter( - conversation=self.conversation, - role=CHATML_ROLE_USER, - created_at__lt=self.created_at, - ) - .latest() - .created_at - ) + return self.saved_run.run_time + # import pandas as pd + + # if self.role == CHATML_ROLE_USER: + # return pd.NaT + # return ( + # self.created_at + # - Message.objects.filter( + # conversation=self.conversation, + # role=CHATML_ROLE_USER, + # created_at__lt=self.created_at, + # ) + # .latest() + # .created_at + # ) class MessageAttachment(models.Model): diff --git a/recipes/VideoBotsStats.py b/recipes/VideoBotsStats.py index 4939944cd..c82989c02 100644 --- a/recipes/VideoBotsStats.py +++ b/recipes/VideoBotsStats.py @@ -31,8 +31,7 @@ TruncYear, Concat, ) -from django.db.models import Count, F, Window -from django.db.models.functions import Lag +from django.db.models import Count, Avg ID_COLUMNS = [ "conversation__fb_page_id", @@ -389,33 +388,6 @@ def calculate_overall_stats(self, bid, bi, run_title, run_url): def calculate_stats_binned_by_time( self, bi, start_date, end_date, factor, trunc_fn ): - average_response_time = ( - Message.objects.filter( - created_at__date__gte=start_date, - created_at__date__lte=end_date, - conversation__bot_integration=bi, - ) - .values("conversation_id") - .order_by("created_at") - .annotate( - response_time=F("created_at") - Window(expression=Lag("created_at")), - ) - .annotate(date=trunc_fn("created_at")) - .values("date", "response_time", "role") - ) - average_response_time = ( - pd.DataFrame( - average_response_time, - columns=["date", "response_time", "role"], - ) - .loc[lambda df: df["role"] == CHATML_ROLE_ASSISTANT] - .groupby("date") - .agg({"response_time": "median"}) - .apply(lambda x: x.clip(lower=timedelta(0))) - .rename(columns={"response_time": "Average_response_time"}) - .reset_index() - ) - messages_received = ( Message.objects.filter( created_at__date__gte=start_date, @@ -436,6 +408,7 @@ def calculate_stats_binned_by_time( distinct=True, ) ) + .annotate(Average_response_time=Avg("saved_run__run_time")) .annotate(Unique_feedback_givers=Count("feedbacks", distinct=True)) .values( "date", @@ -443,6 +416,7 @@ def calculate_stats_binned_by_time( "Convos", "Senders", "Unique_feedback_givers", + "Average_response_time", ) ) @@ -482,6 +456,7 @@ def calculate_stats_binned_by_time( "Convos", "Senders", "Unique_feedback_givers", + "Average_response_time", ], ) df = df.merge( @@ -496,12 +471,6 @@ def calculate_stats_binned_by_time( left_on="date", right_on="date", ) - df = df.merge( - average_response_time, - 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 From 344f6ba493e79a5ee999572186e9f87a12d4c0fc Mon Sep 17 00:00:00 2001 From: Alexander Metzger Date: Thu, 1 Feb 2024 13:01:09 -0800 Subject: [PATCH 09/23] response time collection --- bots/migrations/0056_message_response_time.py | 19 +++++++++++++++ bots/models.py | 23 ++++--------------- daras_ai_v2/bots.py | 12 ++++++++++ recipes/VideoBotsStats.py | 2 +- 4 files changed, 37 insertions(+), 19 deletions(-) create mode 100644 bots/migrations/0056_message_response_time.py diff --git a/bots/migrations/0056_message_response_time.py b/bots/migrations/0056_message_response_time.py new file mode 100644 index 000000000..06730e931 --- /dev/null +++ b/bots/migrations/0056_message_response_time.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.7 on 2024-02-01 20:15 + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bots', '0055_workflowmetadata'), + ] + + operations = [ + migrations.AddField( + model_name='message', + name='response_time', + field=models.DurationField(default=datetime.timedelta(days=-1, seconds=86399), help_text='The time it took for the bot to respond to the corresponding user message'), + ), + ] diff --git a/bots/models.py b/bots/models.py index 204a625bd..22008019f 100644 --- a/bots/models.py +++ b/bots/models.py @@ -1048,6 +1048,11 @@ class Message(models.Model): help_text="Subject of given question (DEPRECATED)", ) + response_time = models.DurationField( + default=datetime.timedelta(seconds=-1), + help_text="The time it took for the bot to respond to the corresponding user message", + ) + _analysis_started = False objects = MessageQuerySet.as_manager() @@ -1063,24 +1068,6 @@ def __str__(self): def local_lang(self): return Truncator(self.display_content).words(30) - @property - def response_time(self): - return self.saved_run.run_time - # import pandas as pd - - # if self.role == CHATML_ROLE_USER: - # return pd.NaT - # return ( - # self.created_at - # - Message.objects.filter( - # conversation=self.conversation, - # role=CHATML_ROLE_USER, - # created_at__lt=self.created_at, - # ) - # .latest() - # .created_at - # ) - class MessageAttachment(models.Model): message = models.ForeignKey( diff --git a/daras_ai_v2/bots.py b/daras_ai_v2/bots.py index 8cc1d69e1..324affbb7 100644 --- a/daras_ai_v2/bots.py +++ b/daras_ai_v2/bots.py @@ -3,11 +3,14 @@ import typing from urllib.parse import parse_qs +import pytz +from datetime import datetime from django.db import transaction from fastapi import HTTPException, Request from furl import furl from sentry_sdk import capture_exception +from daras_ai_v2 import settings from app_users.models import AppUser from bots.models import ( Platform, @@ -202,6 +205,7 @@ def _on_msg(bot: BotInterface): speech_run = None input_images = None input_documents = None + recieved_time: datetime = datetime.now(tz=pytz.timezone(settings.TIME_ZONE)) if not bot.page_cls: bot.send_msg(text=PAGE_NOT_CONNECTED_ERROR) return @@ -286,6 +290,7 @@ def _on_msg(bot: BotInterface): input_documents=input_documents, input_text=input_text, speech_run=speech_run, + recieved_time=recieved_time, ) @@ -324,6 +329,7 @@ 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: @@ -369,6 +375,7 @@ def _process_and_send_msg( platform_msg_id=msg_id, response=response, url=url, + received_time=recieved_time, ) @@ -381,6 +388,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( @@ -396,6 +404,8 @@ def _save_msgs( if speech_run else None ), + response_time=datetime.now(tz=pytz.timezone(settings.TIME_ZONE)) + - received_time, ) attachments = [] for f_url in (input_images or []) + (input_documents or []): @@ -412,6 +422,8 @@ def _save_msgs( saved_run=SavedRun.objects.get_or_create( workflow=Workflow.VIDEO_BOTS, **furl(url).query.params )[0], + response_time=datetime.now(tz=pytz.timezone(settings.TIME_ZONE)) + - received_time, ) # save the messages & attachments with transaction.atomic(): diff --git a/recipes/VideoBotsStats.py b/recipes/VideoBotsStats.py index c82989c02..a265f5a92 100644 --- a/recipes/VideoBotsStats.py +++ b/recipes/VideoBotsStats.py @@ -408,7 +408,7 @@ def calculate_stats_binned_by_time( distinct=True, ) ) - .annotate(Average_response_time=Avg("saved_run__run_time")) + .annotate(Average_response_time=Avg("response_time")) .annotate(Unique_feedback_givers=Count("feedbacks", distinct=True)) .values( "date", From fb9d32cebe0bf3d441acc966fdcd99088e108320 Mon Sep 17 00:00:00 2001 From: Alexander Metzger Date: Thu, 1 Feb 2024 13:26:34 -0800 Subject: [PATCH 10/23] enable show all --- recipes/VideoBotsStats.py | 51 ++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/recipes/VideoBotsStats.py b/recipes/VideoBotsStats.py index a265f5a92..12eabf51f 100644 --- a/recipes/VideoBotsStats.py +++ b/recipes/VideoBotsStats.py @@ -136,7 +136,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 @@ -260,27 +260,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 = datetime.now() + else: + 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") + ) + st.write("---") view = st.horizontal_radio( "### View", options=["Daily", "Weekly", "Monthly"], From 570963c1e99dfedbb6a90192be4c1d601450bb9a Mon Sep 17 00:00:00 2001 From: Alexander Metzger Date: Thu, 1 Feb 2024 13:53:49 -0800 Subject: [PATCH 11/23] tables adhere to data filters --- recipes/VideoBotsStats.py | 60 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/recipes/VideoBotsStats.py b/recipes/VideoBotsStats.py index 12eabf51f..f41add98f 100644 --- a/recipes/VideoBotsStats.py +++ b/recipes/VideoBotsStats.py @@ -210,7 +210,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 +241,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() @@ -642,12 +657,29 @@ def plot_graphs(self, view, df): 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 == "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 == "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 ) @@ -658,6 +690,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 @@ -665,7 +701,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 @@ -673,7 +715,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 @@ -681,7 +729,13 @@ 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 From 747cead27e41e1f683c4b9c658d5e304208c0bf9 Mon Sep 17 00:00:00 2001 From: Alexander Metzger Date: Thu, 1 Feb 2024 14:00:15 -0800 Subject: [PATCH 12/23] round response time --- bots/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bots/models.py b/bots/models.py index 22008019f..edb7e183a 100644 --- a/bots/models.py +++ b/bots/models.py @@ -927,7 +927,7 @@ def to_df_format( else None ), # only show first feedback as per Sean's request "Analysis JSON": message.analysis_result, - "Response Time": message.response_time.total_seconds(), + "Response Time": round(message.response_time.total_seconds(), 1), } rows.append(row) df = pd.DataFrame.from_records( From 819cf1f80fc7fe8fd5ddd8bdaa91051a4224b39b Mon Sep 17 00:00:00 2001 From: Alexander Metzger Date: Thu, 1 Feb 2024 22:02:03 -0800 Subject: [PATCH 13/23] split of graphs --- recipes/VideoBotsStats.py | 80 +++++++++++++++++++++++++++++++++++---- 1 file changed, 72 insertions(+), 8 deletions(-) diff --git a/recipes/VideoBotsStats.py b/recipes/VideoBotsStats.py index f41add98f..08787f1b3 100644 --- a/recipes/VideoBotsStats.py +++ b/recipes/VideoBotsStats.py @@ -499,6 +499,12 @@ def calculate_stats_binned_by_time( 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["Pos_feedback"] + df["Neg_feedback"]) + ) * 100 + df["Percentage_negative_feedback"] = ( + df["Neg_feedback"] / (df["Pos_feedback"] + df["Neg_feedback"]) + ) * 100 df["Msgs_per_convo"] = df["Messages_Sent"] / df["Convos"] df["Msgs_per_user"] = df["Messages_Sent"] / df["Senders"] df["Average_response_time"] = df["Average_response_time"] * factor @@ -602,14 +608,6 @@ def plot_graphs(self, view, df): text=list(df["Msgs_per_user"]), hovertemplate="Messages per User: %{y:.0f}", ), - 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}", - ), ], layout=dict( margin=dict(l=0, r=0, t=28, b=0), @@ -655,6 +653,72 @@ def plot_graphs(self, view, df): ], ) st.plotly_chart(fig) + st.markdown("
", unsafe_allow_html=True) + 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}\\%", + ), + ], + 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) + 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}", + ), + ], + 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) def get_tabular_data( self, From 2a5d31969c86a07e0a6180c5c75829c268253e8d Mon Sep 17 00:00:00 2001 From: Alexander Metzger Date: Thu, 1 Feb 2024 22:50:30 -0800 Subject: [PATCH 14/23] sean tweaks and reordering of graphs --- recipes/VideoBotsStats.py | 134 ++++++++++++++++++++++++++++---------- 1 file changed, 99 insertions(+), 35 deletions(-) diff --git a/recipes/VideoBotsStats.py b/recipes/VideoBotsStats.py index 08787f1b3..718b580f1 100644 --- a/recipes/VideoBotsStats.py +++ b/recipes/VideoBotsStats.py @@ -119,6 +119,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) @@ -153,14 +154,22 @@ def render(self): st.session_state.setdefault("details", self.request.query_params.get("details")) details = st.horizontal_radio( "### Details", - options=[ - "Conversations", - "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", ) @@ -430,7 +439,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", @@ -439,6 +450,8 @@ def calculate_stats_binned_by_time( "Senders", "Unique_feedback_givers", "Average_response_time", + "Average_runtime", + "Average_analysis_time", ) ) @@ -470,6 +483,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=[ @@ -479,6 +506,8 @@ def calculate_stats_binned_by_time( "Senders", "Unique_feedback_givers", "Average_response_time", + "Average_runtime", + "Average_analysis_time", ], ) df = df.merge( @@ -493,6 +522,14 @@ 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 @@ -500,10 +537,13 @@ def calculate_stats_binned_by_time( df["Pos_feedback"] = df["Pos_feedback"] * factor df["Neg_feedback"] = df["Neg_feedback"] * factor df["Percentage_positive_feedback"] = ( - df["Pos_feedback"] / (df["Pos_feedback"] + df["Neg_feedback"]) + df["Pos_feedback"] / df["Messages_Sent"] ) * 100 df["Percentage_negative_feedback"] = ( - df["Neg_feedback"] / (df["Pos_feedback"] + df["Neg_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"] @@ -653,41 +693,41 @@ def plot_graphs(self, view, df): ], ) st.plotly_chart(fig) - st.markdown("
", unsafe_allow_html=True) + st.write("---") fig = go.Figure( data=[ go.Scatter( - name="Positive Feedback", + name="Average Response Time", mode="lines+markers", x=list(df["date"]), - y=list(df["Percentage_positive_feedback"]), - text=list(df["Percentage_positive_feedback"]), - hovertemplate="Positive Feedback: %{y:.0f}\\%", + y=list(df["Average_response_time"]), + text=list(df["Average_response_time"]), + hovertemplate="Average Response Time: %{y:.0f}", ), go.Scatter( - name="Negative Feedback", + name="Average Run Time", mode="lines+markers", x=list(df["date"]), - y=list(df["Percentage_negative_feedback"]), - text=list(df["Percentage_negative_feedback"]), - hovertemplate="Negative Feedback: %{y:.0f}\\%", + 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="Percentage", - range=[0, 100], - tickvals=[ - *range( - 0, - 101, - 10, - ) - ], + title="Seconds", ), title=dict( - text=f"{view} Feedback Distribution", + text=f"{view} Performance Metrics", ), height=300, template="plotly_white", @@ -698,21 +738,45 @@ def plot_graphs(self, view, df): fig = go.Figure( data=[ go.Scatter( - name="Average Response Time", + name="Positive Feedback", 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}", + 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="Seconds", + title="Percentage", + range=[0, 100], + tickvals=[ + *range( + 0, + 101, + 10, + ) + ], ), title=dict( - text=f"{view} Performance Metrics", + text=f"{view} Feedback Distribution", ), height=300, template="plotly_white", From 896e2b808fd9d6d5d5d5514e1b731d7f99b6c867 Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Thu, 1 Feb 2024 00:53:03 +0530 Subject: [PATCH 15/23] optional streaming support for slack & whatsapp add finish_reason to streaming --- README.md | 7 + bots/admin.py | 1 + bots/models.py | 5 + bots/tasks.py | 2 +- daras_ai_v2/base.py | 20 ++- daras_ai_v2/bot_integration_widgets.py | 5 + daras_ai_v2/bots.py | 212 ++++++++++++++----------- daras_ai_v2/facebook_bots.py | 106 +++++-------- daras_ai_v2/language_model.py | 35 ++-- daras_ai_v2/search_ref.py | 2 + daras_ai_v2/slack_bot.py | 94 +++++++---- daras_ai_v2/vector_search.py | 8 +- gooey_ui/pubsub.py | 29 ++++ poetry.lock | 8 +- pyproject.toml | 2 +- recipes/CompareLLM.py | 4 +- recipes/VideoBots.py | 67 +++++--- routers/api.py | 2 +- scripts/test_wa_msg_send.py | 124 +++++++++++++++ 19 files changed, 487 insertions(+), 246 deletions(-) create mode 100644 scripts/test_wa_msg_send.py 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..4f69a1081 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", diff --git a/bots/models.py b/bots/models.py index fd6071f58..467131fa9 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) 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/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..309d32b44 100644 --- a/daras_ai_v2/bot_integration_widgets.py +++ b/daras_ai_v2/bot_integration_widgets.py @@ -24,6 +24,11 @@ def general_integration_settings(bi: BotIntegration): ] = 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..3d106e9ae 100644 --- a/daras_ai_v2/bots.py +++ b/daras_ai_v2/bots.py @@ -20,11 +20,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 +49,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 +62,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 +84,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 +100,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 +151,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.") @@ -326,39 +312,112 @@ def _process_and_send_msg( input_text: str, 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 +425,9 @@ 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), ) @@ -420,45 +479,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..faf364e63 100644 --- a/daras_ai_v2/language_model.py +++ b/daras_ai_v2/language_model.py @@ -340,7 +340,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 +376,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 +396,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( @@ -576,24 +589,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/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/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/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..555778a69 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] diff --git a/recipes/CompareLLM.py b/recipes/CompareLLM.py index c07bcffa6..bb246d6c3 100644 --- a/recipes/CompareLLM.py +++ b/recipes/CompareLLM.py @@ -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/VideoBots.py b/recipes/VideoBots.py index 44994b7ff..452db9f17 100644 --- a/recipes/VideoBots.py +++ b/recipes/VideoBots.py @@ -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: + 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 @@ -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(): 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(), + ) From 2b54d88f5a1e25d6dac6d2fc67acde95f51e9ad1 Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Fri, 2 Feb 2024 18:22:36 +0530 Subject: [PATCH 16/23] migrations --- .../0056_botintegration_streaming_enabled.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 bots/migrations/0056_botintegration_streaming_enabled.py 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", + ), + ), + ] From 4783f7db107d49bc02692b7283bd1fa1bdf7cec9 Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Fri, 2 Feb 2024 18:40:49 +0530 Subject: [PATCH 17/23] fix local variable 'href' referenced before assignment --- recipes/VideoBots.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recipes/VideoBots.py b/recipes/VideoBots.py index 452db9f17..17d05e770 100644 --- a/recipes/VideoBots.py +++ b/recipes/VideoBots.py @@ -1067,7 +1067,7 @@ def messenger_bot_integration(self): if url: href = f'{bi}' else: - f"{bi}" + href = f"{bi}" with st.div(className="mt-2"): st.markdown( f'  {href}', From d2da2d10d84c6ca7e07e964286333cb7847f39dc Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Mon, 5 Feb 2024 20:33:57 +0530 Subject: [PATCH 18/23] use timezone.now() --- daras_ai_v2/bots.py | 13 +++++-------- recipes/VideoBots.py | 6 +++--- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/daras_ai_v2/bots.py b/daras_ai_v2/bots.py index edaf0ed32..9f2ebd555 100644 --- a/daras_ai_v2/bots.py +++ b/daras_ai_v2/bots.py @@ -1,16 +1,15 @@ import mimetypes import traceback import typing +from datetime import datetime from urllib.parse import parse_qs -import pytz -from datetime import datetime 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 -from daras_ai_v2 import settings from app_users.models import AppUser from bots.models import ( Platform, @@ -191,7 +190,7 @@ def _on_msg(bot: BotInterface): speech_run = None input_images = None input_documents = None - recieved_time: datetime = datetime.now(tz=pytz.timezone(settings.TIME_ZONE)) + recieved_time: datetime = timezone.now() if not bot.page_cls: bot.send_msg(text=PAGE_NOT_CONNECTED_ERROR) return @@ -463,8 +462,7 @@ def _save_msgs( if speech_run else None ), - response_time=datetime.now(tz=pytz.timezone(settings.TIME_ZONE)) - - received_time, + response_time=timezone.now() - received_time, ) attachments = [] for f_url in (input_images or []) + (input_documents or []): @@ -481,8 +479,7 @@ def _save_msgs( saved_run=SavedRun.objects.get_or_create( workflow=Workflow.VIDEO_BOTS, **furl(url).query.params )[0], - response_time=datetime.now(tz=pytz.timezone(settings.TIME_ZONE)) - - received_time, + response_time=timezone.now() - received_time, ) # save the messages & attachments with transaction.atomic(): diff --git a/recipes/VideoBots.py b/recipes/VideoBots.py index 5d23502ff..75615e6a7 100644 --- a/recipes/VideoBots.py +++ b/recipes/VideoBots.py @@ -1124,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( """ From b063439037904c706e4101db916f1b68e9fbc366 Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Mon, 5 Feb 2024 20:42:30 +0530 Subject: [PATCH 19/23] make response_time null by default --- bots/migrations/0056_message_response_time.py | 19 --------------- .../0057_message_response_time_and_more.py | 23 +++++++++++++++++++ bots/models.py | 8 +++++-- 3 files changed, 29 insertions(+), 21 deletions(-) delete mode 100644 bots/migrations/0056_message_response_time.py create mode 100644 bots/migrations/0057_message_response_time_and_more.py diff --git a/bots/migrations/0056_message_response_time.py b/bots/migrations/0056_message_response_time.py deleted file mode 100644 index 06730e931..000000000 --- a/bots/migrations/0056_message_response_time.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 4.2.7 on 2024-02-01 20:15 - -import datetime -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('bots', '0055_workflowmetadata'), - ] - - operations = [ - migrations.AddField( - model_name='message', - name='response_time', - field=models.DurationField(default=datetime.timedelta(days=-1, seconds=86399), help_text='The time it took for the bot to respond to the corresponding user message'), - ), - ] 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 4aa2c3d4a..8e2a41dc8 100644 --- a/bots/models.py +++ b/bots/models.py @@ -932,7 +932,10 @@ def to_df_format( else None ), # only show first feedback as per Sean's request "Analysis JSON": message.analysis_result, - "Response Time": round(message.response_time.total_seconds(), 1), + "Response Time": ( + message.response_time + and round(message.response_time.total_seconds(), 1) + ), } rows.append(row) df = pd.DataFrame.from_records( @@ -1054,7 +1057,8 @@ class Message(models.Model): ) response_time = models.DurationField( - default=datetime.timedelta(seconds=-1), + default=None, + null=True, help_text="The time it took for the bot to respond to the corresponding user message", ) From f5466022b9eb943cf5d89bbb72f503b9a22b8df3 Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Mon, 5 Feb 2024 20:56:38 +0530 Subject: [PATCH 20/23] fix run_time display in copilot stats add resposne_time to admin --- bots/admin.py | 2 ++ recipes/VideoBotsStats.py | 17 +++++++---------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/bots/admin.py b/bots/admin.py index 4f69a1081..0b6b475cc 100644 --- a/bots/admin.py +++ b/bots/admin.py @@ -492,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] @@ -551,6 +552,7 @@ def get_fieldsets(self, request, msg: Message = None): "Analysis", { "fields": [ + "response_time", "analysis_result", "analysis_run", "question_answered", diff --git a/recipes/VideoBotsStats.py b/recipes/VideoBotsStats.py index 718b580f1..db88f4dde 100644 --- a/recipes/VideoBotsStats.py +++ b/recipes/VideoBotsStats.py @@ -338,15 +338,12 @@ def render_date_view_inputs(self, bi): 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 @@ -424,7 +421,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")) From 2be2378a5e1142c4b8b8f503850d76154d954882 Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Mon, 5 Feb 2024 21:02:33 +0530 Subject: [PATCH 21/23] use timezone.now() instead of datetime.now() --- recipes/VideoBotsStats.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/recipes/VideoBotsStats.py b/recipes/VideoBotsStats.py index db88f4dde..c18683b96 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 @@ -287,9 +289,9 @@ def render(self): def render_date_view_inputs(self, bi): if st.checkbox("Show All"): start_date = bi.created_at - end_date = datetime.now() + end_date = timezone.now() else: - start_of_year_date = datetime.now().replace(month=1, day=1) + start_of_year_date = timezone.now().replace(month=1, day=1) st.session_state.setdefault( "start_date", self.request.query_params.get( @@ -302,11 +304,11 @@ def render_date_view_inputs(self, bi): st.session_state.setdefault( "end_date", self.request.query_params.get( - "end_date", datetime.now().strftime("%Y-%m-%d") + "end_date", timezone.now().strftime("%Y-%m-%d") ), ) end_date: datetime = ( - st.date_input("End date", key="end_date") or datetime.now() + st.date_input("End date", key="end_date") or timezone.now() ) st.session_state.setdefault( "view", self.request.query_params.get("view", "Weekly") @@ -359,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, @@ -369,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, @@ -544,7 +546,9 @@ def calculate_stats_binned_by_time( ) * 100 df["Msgs_per_convo"] = df["Messages_Sent"] / df["Convos"] df["Msgs_per_user"] = df["Messages_Sent"] / df["Senders"] - df["Average_response_time"] = df["Average_response_time"] * factor + df["Average_response_time"] = ( + df["Average_response_time"].dt.total_seconds() * factor + ) df.fillna(0, inplace=True) df = df.round(0).astype("int32", errors="ignore") return df From 3ac7299c9ed18ac9948ac88768d9d8f74daae0ec Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Mon, 5 Feb 2024 21:06:32 +0530 Subject: [PATCH 22/23] silence black --- recipes/VideoBots.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/recipes/VideoBots.py b/recipes/VideoBots.py index 75615e6a7..5d23502ff 100644 --- a/recipes/VideoBots.py +++ b/recipes/VideoBots.py @@ -1124,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( """ From 4aa53e70bfae3f0b1ee650f4cf945566135ebee9 Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Mon, 5 Feb 2024 21:27:43 +0530 Subject: [PATCH 23/23] fix AttributeError: Can only use .dt accessor with datetimelike values --- recipes/VideoBotsStats.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/recipes/VideoBotsStats.py b/recipes/VideoBotsStats.py index c18683b96..00c7f07dd 100644 --- a/recipes/VideoBotsStats.py +++ b/recipes/VideoBotsStats.py @@ -546,9 +546,11 @@ def calculate_stats_binned_by_time( ) * 100 df["Msgs_per_convo"] = df["Messages_Sent"] / df["Convos"] df["Msgs_per_user"] = df["Messages_Sent"] / df["Senders"] - df["Average_response_time"] = ( - df["Average_response_time"].dt.total_seconds() * factor - ) + 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