diff --git a/bots/admin.py b/bots/admin.py index 524a1c708..23d0a9dd5 100644 --- a/bots/admin.py +++ b/bots/admin.py @@ -294,16 +294,25 @@ class SavedRunAdmin(admin.ModelAdmin): django.db.models.JSONField: {"widget": JSONEditorWidget}, } + def get_queryset(self, request): + return ( + super() + .get_queryset(request) + .prefetch_related( + "parent_version", + "parent_version__published_run", + "parent_version__published_run__saved_run", + ) + ) + def lookup_allowed(self, key, value): if key in ["parent_version__published_run__id__exact"]: return True return super().lookup_allowed(key, value) def view_user(self, saved_run: SavedRun): - return change_obj_url( - AppUser.objects.get(uid=saved_run.uid), - label=f"{saved_run.uid}", - ) + user = AppUser.objects.get(uid=saved_run.uid) + return change_obj_url(user) view_user.short_description = "View User" diff --git a/daras_ai_v2/base.py b/daras_ai_v2/base.py index a921be749..66209281a 100644 --- a/daras_ai_v2/base.py +++ b/daras_ai_v2/base.py @@ -320,7 +320,7 @@ def _render_social_buttons(self, show_button_text: bool = False): copy_to_clipboard_button( f'{button_text}', - value=self._get_current_app_url(), + value=self.get_tab_url(self.tab), type="secondary", className="mb-0 ms-lg-2", ) @@ -1791,8 +1791,8 @@ def run_as_api_tab(self): as_async = st.checkbox("##### Run Async") as_form_data = st.checkbox("##### Upload Files via Form Data") - request_body = get_example_request_body( - self.RequestModel, st.session_state, include_all=include_all + request_body = self.get_example_request_body( + st.session_state, include_all=include_all ) response_body = self.get_example_response_body( st.session_state, as_async=as_async, include_all=include_all @@ -1838,7 +1838,27 @@ def get_price_roundoff(self, state: dict) -> int: return max(1, math.ceil(self.get_raw_price(state))) def get_raw_price(self, state: dict) -> float: - return self.price + return self.price * state.get("num_outputs", 1) + + @classmethod + def get_example_preferred_fields(cls, state: dict) -> list[str]: + """ + Fields that are not required, but are preferred to be shown in the example. + """ + return [] + + @classmethod + def get_example_request_body( + cls, + state: dict, + include_all: bool = False, + ) -> dict: + return extract_model_fields( + cls.RequestModel, + state, + include_all=include_all, + preferred_fields=cls.get_example_preferred_fields(state), + ) def get_example_response_body( self, @@ -1854,6 +1874,7 @@ def get_example_response_body( run_id=run_id, uid=self.request.user and self.request.user.uid, ) + output = extract_model_fields(self.ResponseModel, state, include_all=True) if as_async: return dict( run_id=run_id, @@ -1861,18 +1882,14 @@ def get_example_response_body( created_at=created_at, run_time_sec=st.session_state.get(StateKeys.run_time, 0), status="completed", - output=get_example_request_body( - self.ResponseModel, state, include_all=include_all - ), + output=output, ) else: return dict( id=run_id, url=web_url, created_at=created_at, - output=get_example_request_body( - self.ResponseModel, state, include_all=include_all - ), + output=output, ) def additional_notes(self) -> str | None: @@ -1924,15 +1941,21 @@ def render_output_caption(): ) -def get_example_request_body( - request_model: typing.Type[BaseModel], +def extract_model_fields( + model: typing.Type[BaseModel], state: dict, include_all: bool = False, + preferred_fields: list[str] = None, ) -> dict: + """Only returns required fields unless include_all is set to True.""" return { field_name: state.get(field_name) - for field_name, field in request_model.__fields__.items() - if include_all or field.required + for field_name, field in model.__fields__.items() + if ( + include_all + or field.required + or (preferred_fields and field_name in preferred_fields) + ) } diff --git a/daras_ai_v2/settings.py b/daras_ai_v2/settings.py index d5d2741ae..706946934 100644 --- a/daras_ai_v2/settings.py +++ b/daras_ai_v2/settings.py @@ -35,7 +35,7 @@ HASHIDS_SALT = config("HASHIDS_SALT", default="") ALLOWED_HOSTS = ["*"] -INTERNAL_IPS = ["127.0.0.1"] +INTERNAL_IPS = ["127.0.0.1", "localhost"] SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # Application definition @@ -48,6 +48,7 @@ "django.contrib.staticfiles", "bots", "django_extensions", + # "debug_toolbar", # the order matters, since we want to override the admin templates "django.forms", # needed to override admin forms "django.contrib.admin", @@ -67,6 +68,7 @@ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", + # "debug_toolbar.middleware.DebugToolbarMiddleware", ] ROOT_URLCONF = "gooeysite.urls" diff --git a/gooeysite/urls.py b/gooeysite/urls.py index 6c3809436..fba775500 100644 --- a/gooeysite/urls.py +++ b/gooeysite/urls.py @@ -16,8 +16,9 @@ """ from django.contrib import admin -from django.urls import path +from django.urls import path, include urlpatterns = [ + # path("__debug__/", include("debug_toolbar.urls")), path("", admin.site.urls), ] diff --git a/recipes/CompareLLM.py b/recipes/CompareLLM.py index 2101aece5..7e709a18d 100644 --- a/recipes/CompareLLM.py +++ b/recipes/CompareLLM.py @@ -59,6 +59,10 @@ def preview_image(self, state: dict) -> str | None: def preview_description(self, state: dict) -> str: return "Which language model works best your prompt? Compare your text generations across multiple large language models (LLMs) like OpenAI's evolving and latest ChatGPT engines and others like Curie, Ada, Babbage." + @classmethod + def get_example_preferred_fields(cls, state: dict) -> list[str]: + return ["input_prompt", "selected_models"] + def render_form_v2(self): st.text_area( """ diff --git a/recipes/CompareText2Img.py b/recipes/CompareText2Img.py index dc5ea1ae2..7b1f421de 100644 --- a/recipes/CompareText2Img.py +++ b/recipes/CompareText2Img.py @@ -77,6 +77,10 @@ class ResponseModel(BaseModel): typing.Literal[tuple(e.name for e in Text2ImgModels)], list[str] ] + @classmethod + def get_example_preferred_fields(cls, state: dict) -> list[str]: + return ["selected_models"] + def preview_image(self, state: dict) -> str | None: return DEFAULT_COMPARE_TEXT2IMG_META_IMG @@ -264,4 +268,4 @@ def get_raw_price(self, state: dict) -> int: total += 15 case _: total += 2 - return total + return total * state.get("num_outputs", 1) diff --git a/recipes/DocSearch.py b/recipes/DocSearch.py index 6d7977c25..322cafbd9 100644 --- a/recipes/DocSearch.py +++ b/recipes/DocSearch.py @@ -81,6 +81,10 @@ class ResponseModel(BaseModel): final_prompt: str final_search_query: str | None + @classmethod + def get_example_preferred_fields(self, state: dict) -> list[str]: + return ["documents"] + def render_form_v2(self): st.text_area("#### Search Query", key="search_query") document_uploader("#### Documents") @@ -205,9 +209,10 @@ def run_v2( def get_raw_price(self, state: dict) -> float: name = state.get("selected_model") try: - return llm_price[LargeLanguageModels[name]] * 2 + unit_price = llm_price[LargeLanguageModels[name]] * 2 except KeyError: - return 10 + unit_price = 10 + return unit_price * state.get("num_outputs", 1) def render_documents(state, label="**Documents**", *, key="documents"): diff --git a/recipes/DocSummary.py b/recipes/DocSummary.py index 476bd5da6..8955d5e59 100644 --- a/recipes/DocSummary.py +++ b/recipes/DocSummary.py @@ -80,6 +80,10 @@ class ResponseModel(BaseModel): prompt_tree: PromptTree | None final_prompt: str + @classmethod + def get_example_preferred_fields(cls, state: dict) -> list[str]: + return ["task_instructions", "merge_instructions"] + def preview_image(self, state: dict) -> str | None: return DEFAULT_DOC_SUMMARY_META_IMG diff --git a/recipes/EmailFaceInpainting.py b/recipes/EmailFaceInpainting.py index 4b8b50d1e..385596f6d 100644 --- a/recipes/EmailFaceInpainting.py +++ b/recipes/EmailFaceInpainting.py @@ -88,6 +88,10 @@ class ResponseModel(BaseModel): output_images: list[str] email_sent: bool = False + @classmethod + def get_example_preferred_fields(self, state: dict) -> list[str]: + return ["email_address"] + def preview_image(self, state: dict) -> str | None: return DEFAULT_EMAIL_FACE_INPAINTING_META_IMG diff --git a/recipes/FaceInpainting.py b/recipes/FaceInpainting.py index 6d787bba6..2f1b6a0b0 100644 --- a/recipes/FaceInpainting.py +++ b/recipes/FaceInpainting.py @@ -336,6 +336,8 @@ def get_raw_price(self, state: dict) -> int: selected_model = state.get("selected_model") match selected_model: case InpaintingModels.dall_e.name: - return 20 + unit_price = 20 case _: - return 5 + unit_price = 5 + + return unit_price * state.get("num_outputs", 1) diff --git a/recipes/Img2Img.py b/recipes/Img2Img.py index e2713aaa2..41a1139e4 100644 --- a/recipes/Img2Img.py +++ b/recipes/Img2Img.py @@ -72,6 +72,10 @@ class RequestModel(BaseModel): class ResponseModel(BaseModel): output_images: list[str] + @classmethod + def get_example_preferred_fields(self, state: dict) -> list[str]: + return ["text_prompt"] + def preview_image(self, state: dict) -> str | None: return DEFAULT_IMG2IMG_META_IMG @@ -202,6 +206,8 @@ def get_raw_price(self, state: dict) -> int: selected_model = state.get("selected_model") match selected_model: case Img2ImgModels.dall_e.name: - return 20 + unit_price = 20 case _: - return 5 + unit_price = 5 + + return unit_price * state.get("num_outputs", 1) diff --git a/recipes/ObjectInpainting.py b/recipes/ObjectInpainting.py index a1e0c2449..db12760bf 100644 --- a/recipes/ObjectInpainting.py +++ b/recipes/ObjectInpainting.py @@ -314,6 +314,8 @@ def get_raw_price(self, state: dict) -> int: selected_model = state.get("selected_model") match selected_model: case InpaintingModels.dall_e.name: - return 20 + unit_price = 20 case _: - return 5 + unit_price = 5 + + return unit_price * state.get("num_outputs", 1) diff --git a/recipes/QRCodeGenerator.py b/recipes/QRCodeGenerator.py index b83ab4f2c..2e3a30002 100644 --- a/recipes/QRCodeGenerator.py +++ b/recipes/QRCodeGenerator.py @@ -141,6 +141,15 @@ def related_workflows(self) -> list: EmailFaceInpaintingPage, ] + @classmethod + def get_example_preferred_fields(cls, state: dict) -> list[str]: + if state.get("qr_code_file"): + return ["qr_code_file"] + elif state.get("qr_code_input_image"): + return ["qr_code_input_image"] + else: + return ["qr_code_data"] + def render_form_v2(self): st.text_area( """ @@ -735,7 +744,7 @@ def extract_qr_code_data(img: np.ndarray) -> str: return info -class InvalidQRCode(AssertionError): +class InvalidQRCode(UserError): pass diff --git a/recipes/TextToSpeech.py b/recipes/TextToSpeech.py index f56c3957d..7d8f301fe 100644 --- a/recipes/TextToSpeech.py +++ b/recipes/TextToSpeech.py @@ -81,6 +81,10 @@ class ResponseModel(BaseModel): def fallback_preivew_image(self) -> str | None: return DEFAULT_TTS_META_IMG + @classmethod + def get_example_preferred_fields(cls, state: dict) -> list[str]: + return ["tts_provider"] + def preview_description(self, state: dict) -> str: return "Input your text, pick a voice & a Text-to-Speech AI engine to create audio. Compare the best voice generators from Google, UberDuck.ai & more to add automated voices to your podcast, YouTube videos, website, or app." diff --git a/recipes/VideoBots.py b/recipes/VideoBots.py index 410416342..e368e8c21 100644 --- a/recipes/VideoBots.py +++ b/recipes/VideoBots.py @@ -4,7 +4,7 @@ import os.path import typing -from django.db.models import QuerySet +from django.db.models import QuerySet, Q from furl import furl from pydantic import BaseModel, Field @@ -601,11 +601,13 @@ def get_raw_price(self, state: dict): "raw_tts_text", state.get("raw_output_text", []) ) tts_state = {"text_prompt": "".join(output_text_list)} - return super().get_raw_price(state) + TextToSpeechPage().get_raw_price( + total = super().get_raw_price(state) + TextToSpeechPage().get_raw_price( tts_state ) case _: - return super().get_raw_price(state) + total = super().get_raw_price(state) + + return total * state.get("num_outputs", 1) def additional_notes(self): tts_provider = st.session_state.get("tts_provider") @@ -975,11 +977,11 @@ def render_selected_tab(self, selected_tab): unsafe_allow_html=True, ) - st.write("---") - st.text_input( - "###### 🤖 [Landbot](https://landbot.io/) URL", key="landbot_url" - ) - show_landbot_widget() + # st.write("---") + # st.text_input( + # "###### 🤖 [Landbot](https://landbot.io/) URL", key="landbot_url" + # ) + # show_landbot_widget() def messenger_bot_integration(self): from routers.facebook_api import ig_connect_url, fb_connect_url @@ -1028,15 +1030,28 @@ def messenger_bot_integration(self): st.button("🔄 Refresh") + current_run, published_run = self.get_runs_from_query_params( + *extract_query_params(gooey_get_query_params()) + ) # type: ignore + + integrations_q = Q(billing_account_uid=self.request.user.uid) + + # show admins all the bots connected to the current run + if self.is_current_user_admin(): + integrations_q |= Q(saved_run=current_run) + if published_run: + integrations_q |= Q( + saved_run__example_id=published_run.published_run_id + ) + integrations_q |= Q(published_run=published_run) + integrations: QuerySet[BotIntegration] = BotIntegration.objects.filter( - billing_account_uid=self.request.user.uid + integrations_q ).order_by("platform", "-created_at") + if not integrations: return - current_run, published_run = self.get_runs_from_query_params( - *extract_query_params(gooey_get_query_params()) - ) for bi in integrations: is_connected = (bi.saved_run == current_run) or ( ( diff --git a/recipes/VideoBotsStats.py b/recipes/VideoBotsStats.py index a8fd4e541..4781d4b40 100644 --- a/recipes/VideoBotsStats.py +++ b/recipes/VideoBotsStats.py @@ -33,7 +33,7 @@ TruncYear, Concat, ) -from django.db.models import Count, Avg +from django.db.models import Count, Avg, Q ID_COLUMNS = [ "conversation__fb_page_id", @@ -543,6 +543,7 @@ def calculate_stats_binned_by_time( df["Msgs_per_user"] = df["Messages_Sent"] / df["Senders"] df.fillna(0, inplace=True) df = df.round(0).astype("int32", errors="ignore") + df = df.sort_values(by=["date"], ascending=True).reset_index() return df def plot_graphs(self, view, df): @@ -806,8 +807,6 @@ 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( @@ -818,10 +817,9 @@ def get_tabular_data( df["Bot"] = bi.name elif details == "Answered Successfully": successful_messages: MessageQuerySet = Message.objects.filter( + Q(analysis_result__contains={"Answered": True}) + | Q(analysis_result__contains={"assistant": {"answer": "Found"}}), 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( @@ -832,10 +830,9 @@ def get_tabular_data( df["Bot"] = bi.name elif details == "Answered Unsuccessfully": unsuccessful_messages: MessageQuerySet = Message.objects.filter( + Q(analysis_result__contains={"Answered": False}) + | Q(analysis_result__contains={"assistant": {"answer": "Missing"}}), 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( diff --git a/recipes/asr.py b/recipes/asr.py index 7dcea4249..3542614c8 100644 --- a/recipes/asr.py +++ b/recipes/asr.py @@ -48,6 +48,10 @@ class ResponseModel(BaseModel): raw_output_text: list[str] | None output_text: list[str | AsrOutputJson] + @classmethod + def get_example_preferred_fields(cls, state: dict) -> list[str]: + return ["selected_model", "language", "google_translate_target"] + def preview_image(self, state: dict) -> str | None: return DEFAULT_ASR_META_IMG diff --git a/routers/billing.py b/routers/billing.py index 5f2f2cb06..bf660325b 100644 --- a/routers/billing.py +++ b/routers/billing.py @@ -114,12 +114,16 @@ def account(request: Request): is_admin = request.user.email in settings.ADMIN_EMAILS context = { + "title": "Account • Gooey.AI", "request": request, "settings": settings, "available_subscriptions": available_subscriptions, "user_credits": request.user.balance, "subscription": get_user_subscription(request.user), "is_admin": is_admin, + "canonical_url": str( + furl(settings.APP_BASE_URL) / router.url_path_for(account.__name__) + ), } return templates.TemplateResponse("account.html", context) diff --git a/routers/root.py b/routers/root.py index 0e852a8d9..27cb62934 100644 --- a/routers/root.py +++ b/routers/root.py @@ -26,10 +26,7 @@ from daras_ai_v2.all_pages import all_api_pages, normalize_slug, page_slug_map from daras_ai_v2.api_examples_widget import api_example_generator from daras_ai_v2.asr import FFMPEG_WAV_ARGS, check_wav_audio_format -from daras_ai_v2.base import ( - RedirectException, - get_example_request_body, -) +from daras_ai_v2.base import RedirectException from daras_ai_v2.bots import request_json from daras_ai_v2.copy_to_clipboard_button_widget import copy_to_clipboard_scripts from daras_ai_v2.db import FIREBASE_SESSION_COOKIE @@ -299,9 +296,7 @@ def _api_docs_page(request): page = workflow.page_cls(request=request) state = page.get_root_published_run().saved_run.to_dict() - request_body = get_example_request_body( - page.RequestModel, state, include_all=include_all - ) + request_body = page.get_example_request_body(state, include_all=include_all) response_body = page.get_example_response_body( state, as_async=as_async, include_all=include_all ) diff --git a/scripts/init_self_hosted_pricing.py b/scripts/init_self_hosted_pricing.py index 6ea45c7d3..d1df62a58 100644 --- a/scripts/init_self_hosted_pricing.py +++ b/scripts/init_self_hosted_pricing.py @@ -31,7 +31,7 @@ def run(): add_model(model_ids[m], m.name) except KeyError: pass - add_model("gooey-gpu/wav2lip_gan.pth", "wav2lip") + add_model("wav2lip_gan.pth", "wav2lip") def add_model(model_id, model_name): diff --git a/templates/account.html b/templates/account.html index 2755facc1..6c14bc501 100644 --- a/templates/account.html +++ b/templates/account.html @@ -1,5 +1,11 @@ {% extends 'base.html' %} +{% block head %} + + + +{% endblock %} + {% block content %} -{% endblock content %} \ No newline at end of file +{% endblock content %} diff --git a/templates/base.html b/templates/base.html index e2c180de7..5af87990a 100644 --- a/templates/base.html +++ b/templates/base.html @@ -7,7 +7,8 @@ - {{ title }} + {% block title %}{{ title }}{% endblock title %} + {% block head %}{% endblock head %} diff --git a/templates/login_options.html b/templates/login_options.html index c287f13f0..4825e7451 100644 --- a/templates/login_options.html +++ b/templates/login_options.html @@ -1,7 +1,9 @@ {% extends 'base.html' %} +{% block title %}Login to Gooey.AI{% endblock title %} + {% block head %} - Login - Gooey.AI + {% endblock %} diff --git a/templates/report_email.html b/templates/report_email.html index 098aca9bb..9e3afbf6a 100644 --- a/templates/report_email.html +++ b/templates/report_email.html @@ -19,6 +19,8 @@
Reason for Report: {{ reason_for_report }}

+ In the meantime, you can go through our docs or our video tutorials. +

{% if run_uid != user.uid %} Creator User ID: {{ run_uid }}
diff --git a/tests/test_apis.py b/tests/test_apis.py index 96c27c7ef..ddd6e0e48 100644 --- a/tests/test_apis.py +++ b/tests/test_apis.py @@ -6,10 +6,7 @@ from auth.auth_backend import force_authentication from bots.models import SavedRun, Workflow from daras_ai_v2.all_pages import all_test_pages -from daras_ai_v2.base import ( - BasePage, - get_example_request_body, -) +from daras_ai_v2.base import BasePage from server import app MAX_WORKERS = 20 @@ -27,7 +24,7 @@ def _test_api_sync(page_cls: typing.Type[BasePage]): state = page_cls.recipe_doc_sr().state r = client.post( f"/v2/{page_cls.slug_versions[0]}/", - json=get_example_request_body(page_cls.RequestModel, state), + json=page_cls.get_example_request_body(state), headers={"Authorization": f"Token None"}, allow_redirects=False, ) @@ -45,7 +42,7 @@ def _test_api_async(page_cls: typing.Type[BasePage]): r = client.post( f"/v3/{page_cls.slug_versions[0]}/async/", - json=get_example_request_body(page_cls.RequestModel, state), + json=page_cls.get_example_request_body(state), headers={"Authorization": f"Token None"}, allow_redirects=False, ) @@ -81,7 +78,7 @@ def _test_apis_examples(sr: SavedRun): page_cls = Workflow(sr.workflow).page_cls r = client.post( f"/v2/{page_cls.slug_versions[0]}/?example_id={sr.example_id}", - json=get_example_request_body(page_cls.RequestModel, state), + json=page_cls.get_example_request_body(state), headers={"Authorization": f"Token None"}, allow_redirects=False, )