From fadfde09bf1bc7fc985b7a21512e3cf1f535e10d Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Wed, 3 Jul 2024 20:07:45 +0530 Subject: [PATCH 01/17] show version in published run admin --- bots/admin.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/bots/admin.py b/bots/admin.py index 03ac2cc1e..20927dbf9 100644 --- a/bots/admin.py +++ b/bots/admin.py @@ -269,6 +269,18 @@ def api_integration_stats_url(self, bi: BotIntegration): ) +@admin.register(PublishedRunVersion) +class PublishedRunVersionAdmin(admin.ModelAdmin): + search_fields = ["id", "version_id", "published_run__published_run_id"] + autocomplete_fields = ["published_run", "saved_run", "changed_by"] + + +class PublishedRunVersionInline(admin.TabularInline): + model = PublishedRunVersion + extra = 0 + autocomplete_fields = PublishedRunVersionAdmin.autocomplete_fields + + @admin.register(PublishedRun) class PublishedRunAdmin(admin.ModelAdmin): list_display = [ @@ -290,6 +302,7 @@ class PublishedRunAdmin(admin.ModelAdmin): "created_at", "updated_at", ] + inlines = [PublishedRunVersionInline] def view_user(self, published_run: PublishedRun): if published_run.created_by is None: @@ -425,12 +438,6 @@ def rerun_tasks(self, request, queryset): ) -@admin.register(PublishedRunVersion) -class PublishedRunVersionAdmin(admin.ModelAdmin): - search_fields = ["id", "version_id", "published_run__published_run_id"] - autocomplete_fields = ["published_run", "saved_run", "changed_by"] - - class LastActiveDeltaFilter(admin.SimpleListFilter): title = Conversation.last_active_delta.short_description parameter_name = Conversation.last_active_delta.__name__ From b114ec9aa33318265c0c7c62c497b337af23a01f Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Wed, 3 Jul 2024 20:09:54 +0530 Subject: [PATCH 02/17] use BasePage.RequestModel as the base model for all recipes --- recipes/BulkEval.py | 2 +- recipes/BulkRunner.py | 6 +++--- recipes/ChyronPlant.py | 2 +- recipes/CompareLLM.py | 4 ++-- recipes/CompareText2Img.py | 2 +- recipes/CompareUpscaler.py | 2 +- recipes/DeforumSD.py | 2 +- recipes/DocExtract.py | 2 +- recipes/DocSearch.py | 2 +- recipes/DocSummary.py | 2 +- recipes/EmailFaceInpainting.py | 5 +++-- recipes/FaceInpainting.py | 2 +- recipes/GoogleGPT.py | 2 +- recipes/GoogleImageGen.py | 2 +- recipes/ImageSegmentation.py | 2 +- recipes/Img2Img.py | 2 +- recipes/LetterWriter.py | 2 +- recipes/Lipsync.py | 2 +- recipes/ObjectInpainting.py | 2 +- recipes/QRCodeGenerator.py | 2 +- recipes/SmartGPT.py | 2 +- recipes/SocialLookupEmail.py | 2 +- recipes/Text2Audio.py | 2 +- recipes/TextToSpeech.py | 2 +- recipes/Translation.py | 4 ++-- recipes/VideoBots.py | 2 +- recipes/asr_page.py | 4 ++-- recipes/embeddings_page.py | 2 +- 28 files changed, 35 insertions(+), 34 deletions(-) diff --git a/recipes/BulkEval.py b/recipes/BulkEval.py index b87670784..8c5b8fc19 100644 --- a/recipes/BulkEval.py +++ b/recipes/BulkEval.py @@ -168,7 +168,7 @@ def related_workflows(self) -> list: return [BulkRunnerPage, VideoBotsPage, AsrPage, DocSearchPage] - class RequestModel(LLMSettingsMixin, BaseModel): + class RequestModel(LLMSettingsMixin, BasePage.RequestModel): documents: list[str] = Field( title="Input Data Spreadsheet", description=""" diff --git a/recipes/BulkRunner.py b/recipes/BulkRunner.py index 227674905..6fb6dd5d9 100644 --- a/recipes/BulkRunner.py +++ b/recipes/BulkRunner.py @@ -45,7 +45,7 @@ class BulkRunnerPage(BasePage): slug_versions = ["bulk-runner", "bulk"] price = 1 - class RequestModel(BaseModel): + class RequestModel(BasePage.RequestModel): documents: list[FieldHttpUrl] = Field( title="Input Data Spreadsheet", description=""" @@ -426,7 +426,7 @@ def render_description(self): def render_run_url_inputs(self, key: str, del_key: str, d: dict): from daras_ai_v2.all_pages import all_home_pages - added_options = init_workflow_selector(d, key) + init_workflow_selector(d, key) col1, col2, col3, col4 = st.columns([9, 1, 1, 1], responsive=False) if not d.get("workflow") and d.get("url"): @@ -466,7 +466,7 @@ def render_run_url_inputs(self, key: str, del_key: str, d: dict): options = get_published_run_options( page_cls, current_user=self.request.user ) - options.update(added_options) + options.update(d.get("--added_workflows", {})) with st.div(className="pt-1"): url = st.selectbox( "", diff --git a/recipes/ChyronPlant.py b/recipes/ChyronPlant.py index 116a8ad6a..09b1076b7 100644 --- a/recipes/ChyronPlant.py +++ b/recipes/ChyronPlant.py @@ -14,7 +14,7 @@ class ChyronPlantPage(BasePage): workflow = Workflow.CHYRON_PLANT slug_versions = ["ChyronPlant"] - class RequestModel(BaseModel): + class RequestModel(BasePage.RequestModel): midi_notes: str midi_notes_prompt: str | None diff --git a/recipes/CompareLLM.py b/recipes/CompareLLM.py index 199122524..b1a60157a 100644 --- a/recipes/CompareLLM.py +++ b/recipes/CompareLLM.py @@ -38,7 +38,7 @@ class CompareLLMPage(BasePage): "sampling_temperature": 0.7, } - class RequestModel(BaseModel): + class RequestModel(BasePage.RequestModel): input_prompt: str | None selected_models: ( list[typing.Literal[tuple(e.name for e in LargeLanguageModels)]] | None @@ -141,7 +141,7 @@ def render_example(self, state: dict): st.write("**Prompt**") st.write("```jinja2\n" + state.get("input_prompt", "") + "\n```") for key, value in state.get("variables", {}).items(): - st.text_area(f"`{key}`", value=value, disabled=True) + st.text_area(f"`{key}`", value=str(value), disabled=True) with col2: _render_outputs(state, 300) diff --git a/recipes/CompareText2Img.py b/recipes/CompareText2Img.py index 2059b6302..ee1f4cacd 100644 --- a/recipes/CompareText2Img.py +++ b/recipes/CompareText2Img.py @@ -49,7 +49,7 @@ class CompareText2ImgPage(BasePage): "dall_e_3_style": "vivid", } - class RequestModel(BaseModel): + class RequestModel(BasePage.RequestModel): text_prompt: str negative_prompt: str | None diff --git a/recipes/CompareUpscaler.py b/recipes/CompareUpscaler.py index 42588fe72..875280883 100644 --- a/recipes/CompareUpscaler.py +++ b/recipes/CompareUpscaler.py @@ -21,7 +21,7 @@ class CompareUpscalerPage(BasePage): workflow = Workflow.COMPARE_UPSCALER slug_versions = ["compare-ai-upscalers"] - class RequestModel(BaseModel): + class RequestModel(BasePage.RequestModel): input_image: FieldHttpUrl | None = Field(None, description="Input Image") input_video: FieldHttpUrl | None = Field(None, description="Input Video") diff --git a/recipes/DeforumSD.py b/recipes/DeforumSD.py index 94fe3e799..02d6682de 100644 --- a/recipes/DeforumSD.py +++ b/recipes/DeforumSD.py @@ -180,7 +180,7 @@ class DeforumSDPage(BasePage): selected_model=AnimationModels.protogen_2_2.name, ) - class RequestModel(BaseModel): + class RequestModel(BasePage.RequestModel): # input_prompt: str animation_prompts: AnimationPrompts max_frames: int | None diff --git a/recipes/DocExtract.py b/recipes/DocExtract.py index 48f7c4772..2804ca849 100644 --- a/recipes/DocExtract.py +++ b/recipes/DocExtract.py @@ -76,7 +76,7 @@ class DocExtractPage(BasePage): ] price = 500 - class RequestModel(BaseModel): + class RequestModel(BasePage.RequestModel): documents: list[FieldHttpUrl] sheet_url: FieldHttpUrl | None diff --git a/recipes/DocSearch.py b/recipes/DocSearch.py index 55c86a809..99776b1a1 100644 --- a/recipes/DocSearch.py +++ b/recipes/DocSearch.py @@ -63,7 +63,7 @@ class DocSearchPage(BasePage): "dense_weight": 1.0, } - class RequestModel(DocSearchRequest): + class RequestModel(DocSearchRequest, BasePage.RequestModel): task_instructions: str | None query_instructions: str | None diff --git a/recipes/DocSummary.py b/recipes/DocSummary.py index 90e2aecff..7982cf5de 100644 --- a/recipes/DocSummary.py +++ b/recipes/DocSummary.py @@ -57,7 +57,7 @@ class DocSummaryPage(BasePage): "chain_type": CombineDocumentsChains.map_reduce.name, } - class RequestModel(BaseModel): + class RequestModel(BasePage.RequestModel): documents: list[FieldHttpUrl] task_instructions: str | None diff --git a/recipes/EmailFaceInpainting.py b/recipes/EmailFaceInpainting.py index cb8f3d44f..ab51aba3d 100644 --- a/recipes/EmailFaceInpainting.py +++ b/recipes/EmailFaceInpainting.py @@ -1,7 +1,6 @@ import re import typing -from daras_ai_v2.pydantic_validation import FieldHttpUrl import requests from pydantic import BaseModel @@ -9,8 +8,10 @@ from bots.models import Workflow from daras_ai.image_input import upload_file_from_bytes from daras_ai_v2 import db, settings +from daras_ai_v2.base import BasePage from daras_ai_v2.exceptions import raise_for_status from daras_ai_v2.loom_video_widget import youtube_video +from daras_ai_v2.pydantic_validation import FieldHttpUrl from daras_ai_v2.send_email import send_email_via_postmark from daras_ai_v2.stable_diffusion import InpaintingModels from recipes.FaceInpainting import FaceInpaintingPage @@ -38,7 +39,7 @@ class EmailFaceInpaintingPage(FaceInpaintingPage): "twitter_handle": "seanb", } - class RequestModel(BaseModel): + class RequestModel(BasePage.RequestModel): email_address: str | None twitter_handle: str | None diff --git a/recipes/FaceInpainting.py b/recipes/FaceInpainting.py index d5b34c39b..40e4274e0 100644 --- a/recipes/FaceInpainting.py +++ b/recipes/FaceInpainting.py @@ -44,7 +44,7 @@ class FaceInpaintingPage(BasePage): "upscale_factor": 1.0, } - class RequestModel(BaseModel): + class RequestModel(BasePage.RequestModel): input_image: FieldHttpUrl text_prompt: str diff --git a/recipes/GoogleGPT.py b/recipes/GoogleGPT.py index c094df55c..8328f9dc6 100644 --- a/recipes/GoogleGPT.py +++ b/recipes/GoogleGPT.py @@ -73,7 +73,7 @@ class GoogleGPTPage(BasePage): dense_weight=1.0, ) - class RequestModel(GoogleSearchMixin, BaseModel): + class RequestModel(GoogleSearchMixin, BasePage.RequestModel): search_query: str site_filter: str diff --git a/recipes/GoogleImageGen.py b/recipes/GoogleImageGen.py index 278128a37..86cd58157 100644 --- a/recipes/GoogleImageGen.py +++ b/recipes/GoogleImageGen.py @@ -50,7 +50,7 @@ class GoogleImageGenPage(BasePage): serp_search_location=SerpSearchLocation.UNITED_STATES, ) - class RequestModel(GoogleSearchLocationMixin, BaseModel): + class RequestModel(GoogleSearchLocationMixin, BasePage.RequestModel): search_query: str text_prompt: str diff --git a/recipes/ImageSegmentation.py b/recipes/ImageSegmentation.py index a2b3b35f4..d35256ba2 100644 --- a/recipes/ImageSegmentation.py +++ b/recipes/ImageSegmentation.py @@ -47,7 +47,7 @@ class ImageSegmentationPage(BasePage): "obj_pos_y": 0.5, } - class RequestModel(BaseModel): + class RequestModel(BasePage.RequestModel): input_image: FieldHttpUrl selected_model: ( diff --git a/recipes/Img2Img.py b/recipes/Img2Img.py index aad86d09f..1bee3efd2 100644 --- a/recipes/Img2Img.py +++ b/recipes/Img2Img.py @@ -41,7 +41,7 @@ class Img2ImgPage(BasePage): "controlnet_conditioning_scale": [1.0], } - class RequestModel(BaseModel): + class RequestModel(BasePage.RequestModel): input_image: FieldHttpUrl text_prompt: str | None diff --git a/recipes/LetterWriter.py b/recipes/LetterWriter.py index b4f3432af..76c46add6 100644 --- a/recipes/LetterWriter.py +++ b/recipes/LetterWriter.py @@ -19,7 +19,7 @@ class LetterWriterPage(BasePage): workflow = Workflow.LETTER_WRITER slug_versions = ["LetterWriter"] - class RequestModel(BaseModel): + class RequestModel(BasePage.RequestModel): action_id: str prompt_header: str | None diff --git a/recipes/Lipsync.py b/recipes/Lipsync.py index 1efd41fe6..260ba9630 100644 --- a/recipes/Lipsync.py +++ b/recipes/Lipsync.py @@ -23,7 +23,7 @@ class LipsyncPage(BasePage): workflow = Workflow.LIPSYNC slug_versions = ["Lipsync"] - class RequestModel(LipsyncSettings, BaseModel): + class RequestModel(LipsyncSettings, BasePage.RequestModel): selected_model: typing.Literal[tuple(e.name for e in LipsyncModel)] = ( LipsyncModel.Wav2Lip.name ) diff --git a/recipes/ObjectInpainting.py b/recipes/ObjectInpainting.py index af479c970..ab510e741 100644 --- a/recipes/ObjectInpainting.py +++ b/recipes/ObjectInpainting.py @@ -45,7 +45,7 @@ class ObjectInpaintingPage(BasePage): "seed": 42, } - class RequestModel(BaseModel): + class RequestModel(BasePage.RequestModel): input_image: FieldHttpUrl text_prompt: str diff --git a/recipes/QRCodeGenerator.py b/recipes/QRCodeGenerator.py index ef4de42a2..288eb70a8 100644 --- a/recipes/QRCodeGenerator.py +++ b/recipes/QRCodeGenerator.py @@ -78,7 +78,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.__dict__.update(self.sane_defaults) - class RequestModel(BaseModel): + class RequestModel(BasePage.RequestModel): qr_code_data: str | None qr_code_input_image: FieldHttpUrl | None qr_code_vcard: VCARD | None diff --git a/recipes/SmartGPT.py b/recipes/SmartGPT.py index 2b0f36e67..c4720c9a0 100644 --- a/recipes/SmartGPT.py +++ b/recipes/SmartGPT.py @@ -27,7 +27,7 @@ class SmartGPTPage(BasePage): slug_versions = ["SmartGPT"] price = 20 - class RequestModel(BaseModel): + class RequestModel(BasePage.RequestModel): input_prompt: str cot_prompt: str | None diff --git a/recipes/SocialLookupEmail.py b/recipes/SocialLookupEmail.py index d4cc15f1c..2b4ea0869 100644 --- a/recipes/SocialLookupEmail.py +++ b/recipes/SocialLookupEmail.py @@ -38,7 +38,7 @@ class SocialLookupEmailPage(BasePage): "sampling_temperature": 0.5, } - class RequestModel(BaseModel): + class RequestModel(BasePage.RequestModel): email_address: str input_prompt: str | None diff --git a/recipes/Text2Audio.py b/recipes/Text2Audio.py index 5a00c45cf..c0a851c44 100644 --- a/recipes/Text2Audio.py +++ b/recipes/Text2Audio.py @@ -37,7 +37,7 @@ class Text2AudioPage(BasePage): seed=42, ) - class RequestModel(BaseModel): + class RequestModel(BasePage.RequestModel): text_prompt: str negative_prompt: str | None diff --git a/recipes/TextToSpeech.py b/recipes/TextToSpeech.py index 11c3dd391..9d5e1eebf 100644 --- a/recipes/TextToSpeech.py +++ b/recipes/TextToSpeech.py @@ -85,7 +85,7 @@ class TextToSpeechPage(BasePage): "openai_tts_model": "tts-1", } - class RequestModelBase(BaseModel): + class RequestModelBase(BasePage.RequestModel): text_prompt: str class RequestModel(TextToSpeechSettings, RequestModelBase): diff --git a/recipes/Translation.py b/recipes/Translation.py index 75e0f7d0c..6583f78fa 100644 --- a/recipes/Translation.py +++ b/recipes/Translation.py @@ -39,14 +39,14 @@ class TranslationPage(BasePage): workflow = Workflow.TRANSLATION slug_versions = ["translate", "translation", "compare-ai-translation"] - class BaseRequestModel(BaseModel): + class RequestModelBase(BasePage.RequestModel): texts: list[str] = Field([]) selected_model: ( typing.Literal[tuple(e.name for e in TranslationModels)] ) | None = Field(TranslationModels.google.name) - class RequestModel(TranslationOptions, BaseRequestModel): + class RequestModel(TranslationOptions, RequestModelBase): pass class ResponseModel(BaseModel): diff --git a/recipes/VideoBots.py b/recipes/VideoBots.py index fa4545aab..099b1d72b 100644 --- a/recipes/VideoBots.py +++ b/recipes/VideoBots.py @@ -165,7 +165,7 @@ class VideoBotsPage(BasePage): "translation_model": TranslationModels.google.name, } - class RequestModelBase(BaseModel): + class RequestModelBase(BasePage.RequestModel): input_prompt: str | None input_audio: str | None input_images: list[FieldHttpUrl] | None diff --git a/recipes/asr_page.py b/recipes/asr_page.py index 240f221ca..c18f806d9 100644 --- a/recipes/asr_page.py +++ b/recipes/asr_page.py @@ -41,7 +41,7 @@ class AsrPage(BasePage): sane_defaults = dict(output_format=AsrOutputFormat.text.name) - class BaseRequestModel(BaseModel): + class RequestModelBase(BasePage.RequestModel): documents: list[FieldHttpUrl] selected_model: typing.Literal[tuple(e.name for e in AsrModels)] | None language: str | None @@ -57,7 +57,7 @@ class BaseRequestModel(BaseModel): description="use `translation_model` & `translation_target` instead.", ) - class RequestModel(TranslationOptions, BaseRequestModel): + class RequestModel(TranslationOptions, RequestModelBase): pass class ResponseModel(BaseModel): diff --git a/recipes/embeddings_page.py b/recipes/embeddings_page.py index 2028ce8ec..72bc6f5ac 100644 --- a/recipes/embeddings_page.py +++ b/recipes/embeddings_page.py @@ -19,7 +19,7 @@ class EmbeddingsPage(BasePage): slug_versions = ["embeddings", "embed", "text-embedings"] price = 1 - class RequestModel(BaseModel): + class RequestModel(BasePage.RequestModel): texts: list[str] selected_model: typing.Literal[tuple(e.name for e in EmbeddingModels)] | None From b5cdf120cbdab7e37b72b1ce3c2aed5132c5f10f Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Fri, 5 Jul 2024 23:54:05 +0530 Subject: [PATCH 03/17] save function calls to db and show in steps add pre/post functions to all recipes enable variables in functions lazy load details and steps --- ...workflowmetadata_default_image_and_more.py | 33 +++ bots/models.py | 10 + celeryapp/tasks.py | 2 +- daras_ai_v2/base.py | 138 ++++++++--- daras_ai_v2/custom_enum.py | 31 +++ daras_ai_v2/doc_search_settings_widgets.py | 8 +- daras_ai_v2/functions.py | 70 ------ daras_ai_v2/language_model.py | 2 +- daras_ai_v2/prompt_vars.py | 105 +++++++-- daras_ai_v2/settings.py | 1 + daras_ai_v2/workflow_url_input.py | 10 +- functions/__init__.py | 0 functions/admin.py | 9 + functions/apps.py | 6 + function-executor.js => functions/executor.js | 20 +- functions/migrations/0001_initial.py | 26 +++ functions/migrations/__init__.py | 0 functions/models.py | 63 +++++ functions/recipe_functions.py | 217 ++++++++++++++++++ functions/tests.py | 3 + functions/views.py | 3 + gooey_ui/components/__init__.py | 3 +- recipes/CompareLLM.py | 5 +- recipes/DocSearch.py | 5 +- recipes/Functions.py | 14 +- recipes/GoogleGPT.py | 5 +- recipes/VideoBots.py | 19 +- routers/api.py | 32 +-- scripts/deno-deploy.sh | 5 - 29 files changed, 664 insertions(+), 181 deletions(-) create mode 100644 bots/migrations/0076_alter_workflowmetadata_default_image_and_more.py create mode 100644 daras_ai_v2/custom_enum.py delete mode 100644 daras_ai_v2/functions.py create mode 100644 functions/__init__.py create mode 100644 functions/admin.py create mode 100644 functions/apps.py rename function-executor.js => functions/executor.js (74%) create mode 100644 functions/migrations/0001_initial.py create mode 100644 functions/migrations/__init__.py create mode 100644 functions/models.py create mode 100644 functions/recipe_functions.py create mode 100644 functions/tests.py create mode 100644 functions/views.py delete mode 100755 scripts/deno-deploy.sh diff --git a/bots/migrations/0076_alter_workflowmetadata_default_image_and_more.py b/bots/migrations/0076_alter_workflowmetadata_default_image_and_more.py new file mode 100644 index 000000000..8be0bb4ed --- /dev/null +++ b/bots/migrations/0076_alter_workflowmetadata_default_image_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.7 on 2024-07-05 13:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bots', '0075_alter_publishedrun_workflow_alter_savedrun_workflow_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='workflowmetadata', + name='default_image', + field=models.URLField(blank=True, default='', help_text='Image shown on explore page'), + ), + migrations.AlterField( + model_name='workflowmetadata', + name='help_url', + field=models.URLField(blank=True, default='', help_text='(Not implemented)'), + ), + migrations.AlterField( + model_name='workflowmetadata', + name='meta_keywords', + field=models.JSONField(blank=True, default=list, help_text='(Not implemented)'), + ), + migrations.AlterField( + model_name='workflowmetadata', + name='short_title', + field=models.TextField(help_text='Title used in breadcrumbs'), + ), + ] diff --git a/bots/models.py b/bots/models.py index fbb124726..a2c5715a1 100644 --- a/bots/models.py +++ b/bots/models.py @@ -17,6 +17,7 @@ from bots.custom_fields import PostgresJSONEncoder, CustomURLField from daras_ai_v2.crypto import get_random_doc_id from daras_ai_v2.language_model import format_chat_entry +from functions.models import CalledFunction, CalledFunctionResponse from gooeysite.custom_create import get_or_create_lazy if typing.TYPE_CHECKING: @@ -370,6 +371,15 @@ def get_creator(self) -> AppUser | None: def open_in_gooey(self): return open_in_new_tab(self.get_app_url(), label=self.get_app_url()) + def api_output(self, state: dict = None) -> dict: + state = state or self.state + if self.state.get("functions"): + state["called_functions"] = [ + CalledFunctionResponse.from_db(called_fn) + for called_fn in self.called_functions.all() + ] + return state + def _parse_dt(dt) -> datetime.datetime | None: if isinstance(dt, str): diff --git a/celeryapp/tasks.py b/celeryapp/tasks.py index f535b6ff5..eb6dbeb0a 100644 --- a/celeryapp/tasks.py +++ b/celeryapp/tasks.py @@ -97,7 +97,7 @@ def save(done=False): page.dump_state_to_sr(st.session_state | output, sr) try: - gen = page.run(st.session_state) + gen = page.main(sr, st.session_state) save() while True: # record time diff --git a/daras_ai_v2/base.py b/daras_ai_v2/base.py index 6b641314d..57a9774b4 100644 --- a/daras_ai_v2/base.py +++ b/daras_ai_v2/base.py @@ -1,6 +1,7 @@ import datetime import html import inspect +import json import math import typing import uuid @@ -18,7 +19,7 @@ from fastapi import HTTPException from firebase_admin import auth from furl import furl -from pydantic import BaseModel +from pydantic import BaseModel, Field from sentry_sdk.tracing import ( TRANSACTION_SOURCE_ROUTE, ) @@ -53,6 +54,7 @@ from daras_ai_v2.html_spinner_widget import html_spinner from daras_ai_v2.manage_api_keys_widget import manage_api_keys from daras_ai_v2.meta_preview_url import meta_preview_url +from daras_ai_v2.prompt_vars import variables_input from daras_ai_v2.query_params import ( gooey_get_query_params, ) @@ -64,6 +66,16 @@ from daras_ai_v2.user_date_widgets import ( render_local_dt_attrs, ) +from functions.models import ( + RecipeFunction, + FunctionTrigger, +) +from functions.recipe_functions import ( + functions_input, + call_recipe_functions, + is_functions_enabled, + render_called_functions, +) from gooey_ui import ( realtime_clear_subs, RedirectException, @@ -71,7 +83,6 @@ from gooey_ui.components.modal import Modal from gooey_ui.components.pills import pill from gooey_ui.pubsub import realtime_pull -from gooeysite.custom_create import get_or_create_lazy from routers.account import AccountTabs from routers.root import RecipeTabs @@ -117,7 +128,28 @@ class BasePage: explore_image: str = None - RequestModel: typing.Type[BaseModel] + template_keys: typing.Iterable[str] = ( + "task_instructions", + "query_instructions", + "keyword_instructions", + "input_prompt", + "bot_script", + "text_prompt", + "search_query", + "title", + ) + + class RequestModel(BaseModel): + functions: list[RecipeFunction] | None = Field( + None, + title="🧩 Functions", + ) + variables: dict[str, typing.Any] = Field( + None, + title="âŒĨ Variables", + description="Variables to be used as Jinja prompt templates and in functions as arguments", + ) + ResponseModel: typing.Type[BaseModel] price = settings.CREDITS_TO_DEDUCT_PER_RUN @@ -1310,12 +1342,18 @@ def render_run_cost(self): st.caption(ret, line_clamp=1, unsafe_allow_html=True) def _render_step_row(self): - with st.expander("**ℹī¸ Details**"): + key = "details-expander" + with st.expander("**ℹī¸ Details**", key=key): + if not st.session_state.get(key): + return col1, col2 = st.columns([1, 2]) with col1: self.render_description() with col2: placeholder = st.div() + render_called_functions( + saved_run=self.get_current_sr(), trigger=FunctionTrigger.pre + ) try: self.render_steps() except NotImplementedError: @@ -1323,6 +1361,9 @@ def _render_step_row(self): else: with placeholder: st.write("##### đŸ‘Ŗ Steps") + render_called_functions( + saved_run=self.get_current_sr(), trigger=FunctionTrigger.post + ) def _render_help(self): placeholder = st.div() @@ -1338,9 +1379,13 @@ def _render_help(self): """ ) + key = "discord-expander" with st.expander( - f"**🙋đŸŊ‍♀ī¸ Need more help? [Join our Discord]({settings.DISCORD_INVITE_URL})**" + f"**🙋đŸŊ‍♀ī¸ Need more help? [Join our Discord]({settings.DISCORD_INVITE_URL})**", + key=key, ): + if not st.session_state.get(key): + return st.markdown( """
@@ -1353,6 +1398,27 @@ def _render_help(self): def render_usage_guide(self): raise NotImplementedError + def main(self, sr: SavedRun, state: dict) -> typing.Iterator[str | None]: + yield from call_recipe_functions( + saved_run=sr, + current_user=self.request.user, + request_model=self.RequestModel, + response_model=self.ResponseModel, + state=state, + trigger=FunctionTrigger.pre, + ) + + yield from self.run(state) + + yield from call_recipe_functions( + saved_run=sr, + current_user=self.request.user, + request_model=self.RequestModel, + response_model=self.ResponseModel, + state=state, + trigger=FunctionTrigger.post, + ) + def run(self, state: dict) -> typing.Iterator[str | None]: # initialize request and response request = self.RequestModel.parse_obj(state) @@ -1373,7 +1439,7 @@ def run(self, state: dict) -> typing.Iterator[str | None]: self.ResponseModel.validate(response) def run_v2( - self, request: BaseModel, response: BaseModel + self, request: RequestModel, response: BaseModel ) -> typing.Iterator[str | None]: raise NotImplementedError @@ -1400,8 +1466,11 @@ def update_flag_for_run(self, run_id: str, uid: str, is_flagged: bool): def _render_input_col(self): self.render_form_v2() + self.render_variables() + with st.expander("⚙ī¸ Settings"): self.render_settings() + submitted = self.render_submit_button() with st.div(style={"textAlign": "right"}): st.caption( @@ -1410,6 +1479,13 @@ def _render_input_col(self): ) return submitted + def render_variables(self): + st.write("---") + functions_input(self.request.user) + variables_input( + template_keys=self.template_keys, allow_add=is_functions_enabled() + ) + @classmethod def get_run_state(cls, state: dict[str, typing.Any]) -> RecipeRunState: if state.get(StateKeys.run_status): @@ -1506,7 +1582,7 @@ def on_submit(self): from celeryapp.tasks import auto_recharge try: - example_id, run_id, uid = self.create_new_run(enable_rate_limits=True) + sr = self.create_new_run(enable_rate_limits=True) except RateLimitExceeded as e: st.session_state[StateKeys.run_status] = None st.session_state[StateKeys.error_msg] = e.detail.get("error", "") @@ -1516,15 +1592,15 @@ def on_submit(self): auto_recharge.delay(user_id=self.request.user.id) if settings.CREDITS_TO_DEDUCT_PER_RUN and not self.check_credits(): - st.session_state[StateKeys.run_status] = None - st.session_state[StateKeys.error_msg] = self.generate_credit_error_message( - example_id, run_id, uid + sr.run_status = "" + sr.error_msg = self.generate_credit_error_message( + sr.example_id, sr.run_id, sr.uid ) - self.dump_state_to_sr(st.session_state, self.run_doc_sr(run_id, uid)) + sr.save(update_fields=["run_status", "error_msg"]) else: - self.call_runner_task(example_id, run_id, uid) + self.call_runner_task(sr.example_id, sr.run_id, sr.uid) - raise RedirectException(self.app_url(run_id=run_id, uid=uid)) + raise RedirectException(self.app_url(run_id=sr.run_id, uid=sr.uid)) def should_submit_after_login(self) -> bool: return ( @@ -1534,7 +1610,9 @@ def should_submit_after_login(self) -> bool: and not self.request.user.is_anonymous ) - def create_new_run(self, *, enable_rate_limits: bool = False, **defaults): + def create_new_run( + self, *, enable_rate_limits: bool = False, **defaults + ) -> SavedRun: st.session_state[StateKeys.run_status] = "Starting..." st.session_state.pop(StateKeys.error_msg, None) st.session_state.pop(StateKeys.run_time, None) @@ -1574,9 +1652,23 @@ def create_new_run(self, *, enable_rate_limits: bool = False, **defaults): create=True, defaults=dict(parent=parent, parent_version=parent_version) | defaults, ) - self.dump_state_to_sr(st.session_state, sr) - return None, run_id, uid + # ensure the request is validated + state = st.session_state | json.loads( + self.RequestModel.parse_obj(st.session_state).json(exclude_unset=True) + ) + self.dump_state_to_sr(state, sr) + + return sr + + def dump_state_to_sr(self, state: dict, sr: SavedRun): + sr.set( + { + field_name: deepcopy(state[field_name]) + for field_name in self.fields_to_save() + if field_name in state + } + ) def call_runner_task(self, example_id, run_id, uid, is_api_call=False): from celeryapp.tasks import gui_runner @@ -1673,15 +1765,6 @@ def load_state_defaults(cls, state: dict): state.setdefault(k, v) return state - def dump_state_to_sr(self, state: dict, sr: SavedRun): - sr.set( - { - field_name: deepcopy(state[field_name]) - for field_name in self.fields_to_save() - if field_name in state - } - ) - def fields_to_save(self) -> [str]: # only save the fields in request/response return [ @@ -2058,7 +2141,8 @@ 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) + sr = self.get_current_sr() + output = sr.api_output(extract_model_fields(self.ResponseModel, state)) if as_async: return dict( run_id=run_id, @@ -2146,7 +2230,7 @@ def render_output_caption(): def extract_model_fields( model: typing.Type[BaseModel], state: dict, - include_all: bool = False, + include_all: bool = True, preferred_fields: list[str] = None, diff_from: dict | None = None, ) -> dict: diff --git a/daras_ai_v2/custom_enum.py b/daras_ai_v2/custom_enum.py new file mode 100644 index 000000000..b9aacb843 --- /dev/null +++ b/daras_ai_v2/custom_enum.py @@ -0,0 +1,31 @@ +import typing +from enum import Enum + +import typing_extensions + +T = typing.TypeVar("T", bound="GooeyEnum") + + +class GooeyEnum(Enum): + @classmethod + def db_choices(cls): + return [(e.db_value, e.label) for e in cls] + + @classmethod + def from_db(cls, db_value) -> typing_extensions.Self: + for e in cls: + if e.db_value == db_value: + return e + raise ValueError(f"Invalid {cls.__name__} {db_value=}") + + @classmethod + @property + def api_choices(cls): + return typing.Literal[tuple(e.name for e in cls)] + + @classmethod + def from_api(cls, name: str) -> typing_extensions.Self: + for e in cls: + if e.name == name: + return e + raise ValueError(f"Invalid {cls.__name__} {name=}") diff --git a/daras_ai_v2/doc_search_settings_widgets.py b/daras_ai_v2/doc_search_settings_widgets.py index 00d6c9983..10ae86a7d 100644 --- a/daras_ai_v2/doc_search_settings_widgets.py +++ b/daras_ai_v2/doc_search_settings_widgets.py @@ -10,7 +10,7 @@ from daras_ai_v2.embedding_model import EmbeddingModels from daras_ai_v2.enum_selector_widget import enum_selector from daras_ai_v2.gdrive_downloader import gdrive_list_urls_of_files_in_folder -from daras_ai_v2.prompt_vars import prompt_vars_widget +from daras_ai_v2.prompt_vars import variables_input from daras_ai_v2.search_ref import CitationStyles _user_media_url_prefix = os.path.join( @@ -110,9 +110,6 @@ def query_instructions_widget(): key="query_instructions", height=300, ) - prompt_vars_widget( - "query_instructions", - ) def keyword_instructions_widget(): @@ -124,9 +121,6 @@ def keyword_instructions_widget(): key="keyword_instructions", height=300, ) - prompt_vars_widget( - "keyword_instructions", - ) def doc_extract_selector(current_user: AppUser | None): diff --git a/daras_ai_v2/functions.py b/daras_ai_v2/functions.py deleted file mode 100644 index 2c6c03348..000000000 --- a/daras_ai_v2/functions.py +++ /dev/null @@ -1,70 +0,0 @@ -import json -import tempfile -import typing -from enum import Enum - -from daras_ai.image_input import upload_file_from_bytes -from daras_ai_v2.settings import templates - - -def json_to_pdf(filename: str, data: str) -> str: - html = templates.get_template("form_output.html").render(data=json.loads(data)) - pdf_bytes = html_to_pdf(html) - if not filename.endswith(".pdf"): - filename += ".pdf" - return upload_file_from_bytes(filename, pdf_bytes, "application/pdf") - - -def html_to_pdf(html: str) -> bytes: - from playwright.sync_api import sync_playwright - - with sync_playwright() as p: - browser = p.chromium.launch() - page = browser.new_page() - page.set_content(html) - with tempfile.NamedTemporaryFile(suffix=".pdf") as outfile: - page.pdf(path=outfile.name, format="A4") - ret = outfile.read() - browser.close() - - return ret - - -class LLMTools(Enum): - json_to_pdf = ( - json_to_pdf, - "Save JSON as PDF", - { - "type": "function", - "function": { - "name": json_to_pdf.__name__, - "description": "Save JSON data to PDF", - "parameters": { - "type": "object", - "properties": { - "filename": { - "type": "string", - "description": "A short but descriptive filename for the PDF", - }, - "data": { - "type": "string", - "description": "The JSON data to write to the PDF", - }, - }, - "required": ["filename", "data"], - }, - }, - }, - ) - # send_reply_buttons = (print, "Send back reply buttons to the user.", {}) - - def __new__(cls, fn: typing.Callable, label: str, spec: dict): - obj = object.__new__(cls) - obj._value_ = fn.__name__ - obj.fn = fn - obj.label = label - obj.spec = spec - return obj - - # def __init__(self, *args, **kwargs): - # self._value_ = self.name diff --git a/daras_ai_v2/language_model.py b/daras_ai_v2/language_model.py index 0679f21ac..4ab64fe4e 100644 --- a/daras_ai_v2/language_model.py +++ b/daras_ai_v2/language_model.py @@ -24,7 +24,7 @@ from daras_ai.image_input import gs_url_to_uri, bytes_to_cv2_img, cv2_img_to_bytes from daras_ai_v2.asr import get_google_auth_session from daras_ai_v2.exceptions import raise_for_status, UserError -from daras_ai_v2.functions import LLMTools +from functions.recipe_functions import LLMTools from daras_ai_v2.gpu_server import call_celery_task from daras_ai_v2.text_splitter import ( default_length_function, diff --git a/daras_ai_v2/prompt_vars.py b/daras_ai_v2/prompt_vars.py index b6c2b4795..4ab9878a2 100644 --- a/daras_ai_v2/prompt_vars.py +++ b/daras_ai_v2/prompt_vars.py @@ -1,3 +1,5 @@ +import json +import typing from datetime import datetime from types import SimpleNamespace @@ -8,36 +10,105 @@ import gooey_ui as st -def prompt_vars_widget(*keys: str, variables_key: str = "variables"): +def variables_input( + *, + template_keys: typing.Iterable[str], + label: str = "###### âŒĨ Variables", + key: str = "variables", + allow_add: bool = False, +): + from daras_ai_v2.workflow_url_input import del_button + # find all variables in the prompts env = jinja2.sandbox.SandboxedEnvironment() - template_vars = set() + template_var_names = set() err = None - for k in keys: + for k in template_keys: try: parsed = env.parse(st.session_state.get(k, "")) except jinja2.exceptions.TemplateSyntaxError as e: err = e else: - template_vars |= jinja2.meta.find_undeclared_variables(parsed) + template_var_names |= jinja2.meta.find_undeclared_variables(parsed) + + old_vars = st.session_state.get(key, {}) + + var_add_key = f"--{key}:add_btn" + var_name_key = f"--{key}:add_name" + if st.session_state.pop(var_add_key, None): + if var_name := st.session_state.pop(var_name_key, None): + old_vars[var_name] = "" - # don't mistake globals for vars - template_vars -= set(context_globals().keys()) + all_var_names = ( + (template_var_names | set(old_vars)) + - set(context_globals().keys()) # dont show global context variables + - set(st.session_state.keys()) # dont show other session state variables + ) - if not (template_vars or err): - return + st.session_state[key] = new_vars = {} + title_shown = False + for name in sorted(all_var_names): + var_key = f"--{key}:{name}" - st.write("###### âŒĨ Variables") - old_state = st.session_state.get(variables_key, {}) - new_state = {} - for name in sorted(template_vars): - if name in st.session_state: + del_key = f"--{var_key}:del" + if st.session_state.get(del_key, None): continue - var_key = f"__{variables_key}_{name}" - st.session_state.setdefault(var_key, old_state.get(name, "")) - new_state[name] = st.text_area("`" + name + "`", key=var_key, height=300) - st.session_state[variables_key] = new_state + + if not title_shown: + st.write(label) + title_shown = True + + col1, col2 = st.columns([11, 1], responsive=False) + with col1: + value = old_vars.get(name) + try: + new_text_value = st.session_state[var_key] + except KeyError: + if value is None: + value = "" + is_json = isinstance(value, (dict, list)) + if is_json: + value = json.dumps(value, indent=2) + st.session_state[var_key] = str(value) + else: + try: + value = json.loads(new_text_value) + is_json = isinstance(value, (dict, list)) + if not is_json: + value = new_text_value + except json.JSONDecodeError: + is_json = False + value = new_text_value + new_vars[name] = value + + st.text_area( + "**```" + name + "```**" + (" (JSON)" if is_json else ""), + key=var_key, + height=300, + ) + if name not in template_var_names: + with col2, st.div(className="pt-3 mt-4"): + del_button(key=del_key) + + if allow_add: + if not title_shown: + st.write(label) + st.newline() + col1, col2, _ = st.columns([6, 2, 4], responsive=False) + with col1: + with st.div(style=dict(fontFamily="var(--bs-font-monospace)")): + st.text_input( + "", + key=var_name_key, + placeholder="my_var_name", + ) + with col2: + st.button( + ' Add', + key=var_add_key, + type="tertiary", + ) if err: st.error(f"{type(err).__qualname__}: {err.message}") diff --git a/daras_ai_v2/settings.py b/daras_ai_v2/settings.py index bec46ad4f..58878a9af 100644 --- a/daras_ai_v2/settings.py +++ b/daras_ai_v2/settings.py @@ -62,6 +62,7 @@ "embeddings", "handles", "payments", + "functions", ] MIDDLEWARE = [ diff --git a/daras_ai_v2/workflow_url_input.py b/daras_ai_v2/workflow_url_input.py index 76a7e6142..fe4871d79 100644 --- a/daras_ai_v2/workflow_url_input.py +++ b/daras_ai_v2/workflow_url_input.py @@ -23,7 +23,7 @@ def workflow_url_input( current_user: AppUser | None = None, allow_none: bool = False, ) -> tuple[typing.Type[BasePage], SavedRun, PublishedRun | None] | None: - added_options = init_workflow_selector(internal_state, key) + init_workflow_selector(internal_state, key) col1, col2, col3, col4 = st.columns([9, 1, 1, 1], responsive=False) if not internal_state.get("workflow") and internal_state.get("url"): @@ -40,7 +40,7 @@ def workflow_url_input( internal_state["workflow"] = page_cls.workflow with col1: options = get_published_run_options(page_cls, current_user=current_user) - options.update(added_options) + options.update(internal_state.get("--added_workflows", {})) with st.div(className="pt-1"): url = st.selectbox( "", @@ -109,7 +109,7 @@ def init_workflow_selector( try: _, sr, pr = url_to_runs(str(internal_state["url"])) except Exception: - return {} + return workflow = sr.workflow page_cls = Workflow(workflow).page_cls @@ -122,9 +122,7 @@ def init_workflow_selector( internal_state["workflow"] = workflow internal_state["url"] = url - return {url: title} - - return {} + internal_state.setdefault("--added_workflows", {})[url] = title def url_to_runs( diff --git a/functions/__init__.py b/functions/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/functions/admin.py b/functions/admin.py new file mode 100644 index 000000000..977ee15ef --- /dev/null +++ b/functions/admin.py @@ -0,0 +1,9 @@ +from django.contrib import admin + +from functions.models import CalledFunction + + +# Register your models here. +@admin.register(CalledFunction) +class CalledFunctionAdmin(admin.ModelAdmin): + autocomplete_fields = ["saved_run", "function_run"] diff --git a/functions/apps.py b/functions/apps.py new file mode 100644 index 000000000..3c317752d --- /dev/null +++ b/functions/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class FunctionsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "functions" diff --git a/function-executor.js b/functions/executor.js similarity index 74% rename from function-executor.js rename to functions/executor.js index 8bcc63dd6..097cf8123 100644 --- a/function-executor.js +++ b/functions/executor.js @@ -1,17 +1,21 @@ +// +// To update this, run: +// deployctl deploy --include functions/executor.js functions/executor.js --prod +// (Exclude --prod when testing in development) +// Deno.serve(async (req) => { if (!isAuthenticated(req)) { return new Response("Unauthorized", { status: 401 }); } let logs = captureConsole(); - let code = await req.json(); + let { code, variables } = await req.json(); let status, response; try { - let Deno = undefined; // Deno should not available to user code - let retval = eval(code); + let retval = isolatedEval(code, variables); if (retval instanceof Function) { - retval = retval(); + retval = retval(variables); } if (retval instanceof Promise) { retval = await retval; @@ -27,6 +31,14 @@ Deno.serve(async (req) => { return new Response(body, { status }); }); +function isolatedEval(code, variables) { + // Hide global objects + let Deno = undefined; + let globalThis = undefined; + let window = undefined; + return eval(code); +} + function isAuthenticated(req) { let authorization = req.headers.get("Authorization"); if (!authorization) return false; diff --git a/functions/migrations/0001_initial.py b/functions/migrations/0001_initial.py new file mode 100644 index 000000000..b04707d16 --- /dev/null +++ b/functions/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.7 on 2024-07-05 13:44 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('bots', '0076_alter_workflowmetadata_default_image_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='CalledFunction', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('trigger', models.IntegerField(choices=[(1, 'Pre'), (2, 'Post')])), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('function_run', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='called_by_runs', to='bots.savedrun')), + ('saved_run', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='called_functions', to='bots.savedrun')), + ], + ), + ] diff --git a/functions/migrations/__init__.py b/functions/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/functions/models.py b/functions/models.py new file mode 100644 index 000000000..0901fb109 --- /dev/null +++ b/functions/models.py @@ -0,0 +1,63 @@ +import typing + +from django.db import models +from pydantic import BaseModel, Field + +from daras_ai_v2.custom_enum import GooeyEnum +from daras_ai_v2.pydantic_validation import FieldHttpUrl + + +class _TriggerData(typing.NamedTuple): + label: str + db_value: int + + +class FunctionTrigger(_TriggerData, GooeyEnum): + pre = _TriggerData(label="Pre", db_value=1) + post = _TriggerData(label="Post", db_value=2) + + +class RecipeFunction(BaseModel): + url: FieldHttpUrl = Field( + title="URL", + description="The URL of the [function](https://gooey.ai/functions) to call.", + ) + trigger: FunctionTrigger.api_choices = Field( + title="Trigger", + description="When to run this function. `pre` runs before the recipe, `post` runs after the recipe.", + ) + + +class CalledFunctionResponse(BaseModel): + url: str + trigger: FunctionTrigger.api_choices + return_value: typing.Any + + @classmethod + def from_db(cls, called_fn: "CalledFunction") -> "CalledFunctionResponse": + return cls( + url=called_fn.function_run.get_app_url(), + trigger=FunctionTrigger.from_db(called_fn.trigger).name, + return_value=called_fn.function_run.state.get("return_value"), + ) + + +class CalledFunction(models.Model): + saved_run = models.ForeignKey( + "bots.SavedRun", + on_delete=models.CASCADE, + related_name="called_functions", + ) + function_run = models.ForeignKey( + "bots.SavedRun", + on_delete=models.CASCADE, + related_name="called_by_runs", + ) + trigger = models.IntegerField( + choices=FunctionTrigger.db_choices(), + ) + + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"{self.saved_run} -> {self.function_run} ({self.trigger})" diff --git a/functions/recipe_functions.py b/functions/recipe_functions.py new file mode 100644 index 000000000..ff3c2f734 --- /dev/null +++ b/functions/recipe_functions.py @@ -0,0 +1,217 @@ +import json +import tempfile +import typing +from enum import Enum + +from pydantic import BaseModel + +import gooey_ui as st +from app_users.models import AppUser +from daras_ai.image_input import upload_file_from_bytes +from daras_ai_v2.enum_selector_widget import enum_selector +from daras_ai_v2.field_render import field_title_desc +from daras_ai_v2.settings import templates +from functions.models import CalledFunction, FunctionTrigger + +if typing.TYPE_CHECKING: + from bots.models import SavedRun + + +def call_recipe_functions( + *, + saved_run: "SavedRun", + current_user: AppUser, + request_model: typing.Type[BaseModel], + response_model: typing.Type[BaseModel], + state: dict, + trigger: FunctionTrigger, +): + from daras_ai_v2.workflow_url_input import url_to_runs + from gooeysite.bg_db_conn import get_celery_result_db_safe + + request = request_model.parse_obj(state) + + functions = getattr(request, "functions", None) or [] + functions = [fun for fun in functions if fun.trigger == trigger.name] + if not functions: + return + variables = state.setdefault("variables", {}) + + yield f"Running {trigger.name} hooks..." + + for fun in functions: + # run the function + page_cls, sr, pr = url_to_runs(fun.url) + result, sr = sr.submit_api_call( + current_user=current_user, + request_body=dict( + variables=sr.state.get("variables", {}) + | variables + | dict( + request=json.loads( + request.json(exclude_unset=True, exclude={"variables"}) + ), + response={ + k: v for k, v in state.items() if k in response_model.__fields__ + }, + ), + ), + ) + + CalledFunction.objects.create( + saved_run=saved_run, function_run=sr, trigger=trigger.db_value + ) + + # wait for the result if its a pre request function + if trigger == FunctionTrigger.post: + continue + get_celery_result_db_safe(result) + sr.refresh_from_db() + # if failed, raise error + if sr.error_msg: + raise RuntimeError(sr.error_msg) + + # save the output from the function + return_value = sr.state.get("return_value") + if return_value is None: + continue + if isinstance(return_value, dict): + for k, v in return_value.items(): + if k in request_model.__fields__ or k in response_model.__fields__: + state[k] = v + else: + variables[k] = v + else: + variables["return_value"] = return_value + + +def render_called_functions(*, saved_run: "SavedRun", trigger: FunctionTrigger): + from recipes.Functions import FunctionsPage + from daras_ai_v2.breadcrumbs import get_title_breadcrumbs + + if not is_functions_enabled(): + return + qs = saved_run.called_functions.filter(trigger=trigger.db_value) + if not qs.exists(): + return + for called_fn in qs: + tb = get_title_breadcrumbs( + FunctionsPage, + called_fn.function_run, + called_fn.function_run.parent_published_run(), + ) + title = (tb.published_title and tb.published_title.title) or tb.h1_title + st.write(f"###### 🧩 Called [{title}]({called_fn.function_run.get_app_url()})") + return_value = called_fn.function_run.state.get("return_value") + if return_value is not None: + st.json(return_value) + st.newline() + + +def is_functions_enabled(key="functions") -> bool: + return bool(st.session_state.get(f"--enable-{key}")) + + +def functions_input(current_user: AppUser, key="functions"): + from recipes.BulkRunner import list_view_editor + from daras_ai_v2.base import BasePage + + def render_function_input(list_key: str, del_key: str, d: dict): + from daras_ai_v2.workflow_url_input import workflow_url_input + from recipes.Functions import FunctionsPage + + col1, col2 = st.columns([2, 10], responsive=False) + with col1: + col1.node.props["className"] += " pt-1" + d["trigger"] = enum_selector( + enum_cls=FunctionTrigger, + use_selectbox=True, + key=list_key + ":trigger", + value=d.get("trigger"), + ) + with col2: + workflow_url_input( + page_cls=FunctionsPage, + key=list_key + ":url", + internal_state=d, + del_key=del_key, + current_user=current_user, + ) + + if st.checkbox( + f"##### {field_title_desc(BasePage.RequestModel, key)}", + key=f"--enable-{key}", + value=key in st.session_state, + ): + st.session_state.setdefault(key, [{}]) + list_view_editor( + add_btn_label="➕ Add Function", + key=key, + render_inputs=render_function_input, + ) + st.write("---") + else: + st.session_state.pop(key, None) + + +def json_to_pdf(filename: str, data: str) -> str: + html = templates.get_template("form_output.html").render(data=json.loads(data)) + pdf_bytes = html_to_pdf(html) + if not filename.endswith(".pdf"): + filename += ".pdf" + return upload_file_from_bytes(filename, pdf_bytes, "application/pdf") + + +def html_to_pdf(html: str) -> bytes: + from playwright.sync_api import sync_playwright + + with sync_playwright() as p: + browser = p.chromium.launch() + page = browser.new_page() + page.set_content(html) + with tempfile.NamedTemporaryFile(suffix=".pdf") as outfile: + page.pdf(path=outfile.name, format="A4") + ret = outfile.read() + browser.close() + + return ret + + +class LLMTools(Enum): + json_to_pdf = ( + json_to_pdf, + "Save JSON as PDF", + { + "type": "function", + "function": { + "name": json_to_pdf.__name__, + "description": "Save JSON data to PDF", + "parameters": { + "type": "object", + "properties": { + "filename": { + "type": "string", + "description": "A short but descriptive filename for the PDF", + }, + "data": { + "type": "string", + "description": "The JSON data to write to the PDF", + }, + }, + "required": ["filename", "data"], + }, + }, + }, + ) + # send_reply_buttons = (print, "Send back reply buttons to the user.", {}) + + def __new__(cls, fn: typing.Callable, label: str, spec: dict): + obj = object.__new__(cls) + obj._value_ = fn.__name__ + obj.fn = fn + obj.label = label + obj.spec = spec + return obj + + # def __init__(self, *args, **kwargs): + # self._value_ = self.name diff --git a/functions/tests.py b/functions/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/functions/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/functions/views.py b/functions/views.py new file mode 100644 index 000000000..91ea44a21 --- /dev/null +++ b/functions/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/gooey_ui/components/__init__.py b/gooey_ui/components/__init__.py index fdeb5183f..c87e3a90d 100644 --- a/gooey_ui/components/__init__.py +++ b/gooey_ui/components/__init__.py @@ -551,12 +551,13 @@ def anchor( form_submit_button = button -def expander(label: str, *, expanded: bool = False, **props): +def expander(label: str, *, expanded: bool = False, key: str = None, **props): node = state.RenderTreeNode( name="expander", props=dict( label=dedent(label), open=expanded, + name=key or md5_values(label, expanded, props), **props, ), ) diff --git a/recipes/CompareLLM.py b/recipes/CompareLLM.py index b1a60157a..3712a6b8f 100644 --- a/recipes/CompareLLM.py +++ b/recipes/CompareLLM.py @@ -17,7 +17,7 @@ ) from daras_ai_v2.language_model_settings_widgets import language_model_settings from daras_ai_v2.loom_video_widget import youtube_video -from daras_ai_v2.prompt_vars import prompt_vars_widget, render_prompt_vars +from daras_ai_v2.prompt_vars import variables_input, render_prompt_vars DEFAULT_COMPARE_LM_META_IMG = "https://storage.googleapis.com/dara-c1b52.appspot.com/daras_ai/media/fef06d86-1f70-11ef-b8ee-02420a00015b/LLMs.jpg" @@ -50,8 +50,6 @@ class RequestModel(BasePage.RequestModel): max_tokens: int | None sampling_temperature: float | None - variables: dict[str, typing.Any] | None - response_format_type: ResponseFormatType = Field( None, title="Response Format", @@ -81,7 +79,6 @@ def render_form_v2(self): help="What a fine day..", height=300, ) - prompt_vars_widget("input_prompt") enum_multiselect( LargeLanguageModels, diff --git a/recipes/DocSearch.py b/recipes/DocSearch.py index 99776b1a1..b864d6700 100644 --- a/recipes/DocSearch.py +++ b/recipes/DocSearch.py @@ -22,7 +22,7 @@ ) from daras_ai_v2.language_model_settings_widgets import language_model_settings from daras_ai_v2.loom_video_widget import youtube_video -from daras_ai_v2.prompt_vars import prompt_vars_widget, render_prompt_vars +from daras_ai_v2.prompt_vars import variables_input, render_prompt_vars from daras_ai_v2.query_generator import generate_final_search_query from daras_ai_v2.search_ref import ( SearchReference, @@ -78,8 +78,6 @@ class RequestModel(DocSearchRequest, BasePage.RequestModel): citation_style: typing.Literal[tuple(e.name for e in CitationStyles)] | None - variables: dict[str, typing.Any] | None - class ResponseModel(BaseModel): output_text: list[str] @@ -94,7 +92,6 @@ def get_example_preferred_fields(self, state: dict) -> list[str]: def render_form_v2(self): st.text_area("#### Search Query", key="search_query") bulk_documents_uploader("#### Documents") - prompt_vars_widget("task_instructions", "query_instructions") def validate_form_v2(self): search_query = st.session_state.get("search_query", "").strip() diff --git a/recipes/Functions.py b/recipes/Functions.py index c0ea22de5..b2b1c23dd 100644 --- a/recipes/Functions.py +++ b/recipes/Functions.py @@ -1,4 +1,3 @@ -import json import typing import requests @@ -9,6 +8,7 @@ from daras_ai_v2 import settings from daras_ai_v2.base import BasePage from daras_ai_v2.field_render import field_title_desc +from daras_ai_v2.prompt_vars import variables_input class ConsoleLogs(BaseModel): @@ -27,6 +27,11 @@ class RequestModel(BaseModel): title="Code", description="The JS code to be executed.", ) + variables: dict[str, typing.Any] = Field( + {}, + title="Variables", + description="Variables to be used in the code", + ) class ResponseModel(BaseModel): return_value: typing.Any = Field( @@ -51,10 +56,11 @@ def run_v2( response: "FunctionsPage.ResponseModel", ) -> typing.Iterator[str | None]: yield "Running your code..." + # this will run functions/executor.js in deno deploy r = requests.post( settings.DENO_FUNCTIONS_URL, headers={"Authorization": f"Basic {settings.DENO_FUNCTIONS_AUTH_TOKEN}"}, - json=request.code, + json=dict(code=request.code, variables=request.variables or {}), ) data = r.json() response.logs = data.get("logs") @@ -67,9 +73,11 @@ def render_form_v2(self): st.text_area( "##### " + field_title_desc(self.RequestModel, "code"), key="code", - height=500, ) + def render_variables(self): + variables_input(template_keys=["code"], allow_add=True) + def render_output(self): if error := st.session_state.get("error"): with st.tag("pre", className="bg-danger bg-opacity-25"): diff --git a/recipes/GoogleGPT.py b/recipes/GoogleGPT.py index 8328f9dc6..270e53f2f 100644 --- a/recipes/GoogleGPT.py +++ b/recipes/GoogleGPT.py @@ -17,7 +17,7 @@ ) from daras_ai_v2.language_model_settings_widgets import language_model_settings from daras_ai_v2.loom_video_widget import youtube_video -from daras_ai_v2.prompt_vars import render_prompt_vars, prompt_vars_widget +from daras_ai_v2.prompt_vars import render_prompt_vars, variables_input from daras_ai_v2.query_generator import generate_final_search_query from daras_ai_v2.search_ref import ( SearchReference, @@ -100,8 +100,6 @@ class RequestModel(GoogleSearchMixin, BasePage.RequestModel): "dense_weight" ].field_info - variables: dict[str, typing.Any] | None - class ResponseModel(BaseModel): output_text: list[str] @@ -115,7 +113,6 @@ class ResponseModel(BaseModel): def render_form_v2(self): st.text_area("#### Google Search Query", key="search_query") st.text_input("Search on a specific site *(optional)*", key="site_filter") - prompt_vars_widget("task_instructions", "query_instructions") def validate_form_v2(self): assert st.session_state.get( diff --git a/recipes/VideoBots.py b/recipes/VideoBots.py index 099b1d72b..f8a3b8534 100644 --- a/recipes/VideoBots.py +++ b/recipes/VideoBots.py @@ -52,7 +52,6 @@ from daras_ai_v2.enum_selector_widget import enum_selector from daras_ai_v2.exceptions import UserError from daras_ai_v2.field_render import field_title_desc, field_desc, field_title -from daras_ai_v2.functions import LLMTools from daras_ai_v2.glossary import validate_glossary_document from daras_ai_v2.language_model import ( run_language_model, @@ -71,7 +70,7 @@ from daras_ai_v2.lipsync_api import LipsyncSettings, LipsyncModel from daras_ai_v2.lipsync_settings_widgets import lipsync_settings from daras_ai_v2.loom_video_widget import youtube_video -from daras_ai_v2.prompt_vars import render_prompt_vars, prompt_vars_widget +from daras_ai_v2.prompt_vars import render_prompt_vars from daras_ai_v2.pydantic_validation import FieldHttpUrl from daras_ai_v2.query_generator import generate_final_search_query from daras_ai_v2.query_params import gooey_get_query_params @@ -89,6 +88,7 @@ text_to_speech_provider_selector, ) from daras_ai_v2.vector_search import DocSearchRequest +from functions.recipe_functions import LLMTools from gooey_ui import RedirectException from recipes.DocSearch import ( get_top_k_references, @@ -247,8 +247,6 @@ class RequestModelBase(BasePage.RequestModel): LipsyncModel.Wav2Lip.name ) - variables: dict[str, typing.Any] | None - tools: list[LLMTools] | None = Field( title="🛠ī¸ Tools", description="Give your copilot superpowers by giving it access to tools. Powered by [Function calling](https://platform.openai.com/docs/guides/function-calling).", @@ -331,9 +329,6 @@ def render_form_v2(self): key="bot_script", height=300, ) - prompt_vars_widget( - "bot_script", - ) enum_selector( LargeLanguageModels, @@ -519,9 +514,6 @@ def render_settings(self): key="task_instructions", height=300, ) - prompt_vars_widget( - "task_instructions", - ) citation_style_selector() st.checkbox("🔗 Shorten Citation URLs", key="use_url_shortener") @@ -604,7 +596,10 @@ def render_output(self): references = st.session_state.get("references", []) if not references: return - with st.expander("💁‍♀ī¸ Sources"): + key = "sources-expander" + with st.expander("💁‍♀ī¸ Sources", key=key): + if not st.session_state.get(key): + return for idx, ref in enumerate(references): st.write(f"**{idx + 1}**. [{ref['title']}]({ref['url']})") text_output( @@ -691,7 +686,7 @@ def render_steps(self): st.json(final_prompt) for k in ["raw_output_text", "output_text", "raw_tts_text"]: - for idx, text in enumerate(st.session_state.get(k, [])): + for idx, text in enumerate(st.session_state.get(k) or []): st.text_area( f"###### `{k}[{idx}]`", value=text, diff --git a/routers/api.py b/routers/api.py index 979af1c3a..dfc96d1cd 100644 --- a/routers/api.py +++ b/routers/api.py @@ -19,22 +19,21 @@ from starlette.datastructures import UploadFile from starlette.requests import Request -from celeryapp.tasks import auto_recharge -from daras_ai_v2.auto_recharge import user_should_auto_recharge import gooey_ui as st from app_users.models import AppUser from auth.token_authentication import api_auth_header from bots.models import RetentionPolicy +from celeryapp.tasks import auto_recharge from daras_ai.image_input import upload_file_from_bytes from daras_ai_v2 import settings from daras_ai_v2.all_pages import all_api_pages +from daras_ai_v2.auto_recharge import user_should_auto_recharge from daras_ai_v2.base import ( BasePage, - StateKeys, RecipeRunState, ) from daras_ai_v2.fastapi_tricks import fastapi_request_form -from daras_ai_v2.ratelimits import ensure_rate_limits +from functions.models import CalledFunctionResponse from gooeysite.bg_db_conn import get_celery_result_db_safe from routers.account import AccountTabs @@ -119,9 +118,14 @@ def script_to_api(page_cls: typing.Type[BasePage]): settings=(RunSettings, RunSettings()), ) # encapsulate the response model with the ApiResponseModel + response_output_model = create_model( + page_cls.__name__ + "Output", + __base__=page_cls.ResponseModel, + called_functions=(list[CalledFunctionResponse], None), + ) response_model = create_model( page_cls.__name__ + "Response", - __base__=ApiResponseModelV2[page_cls.ResponseModel], + __base__=ApiResponseModelV2[response_output_model], ) common_errs = { @@ -244,7 +248,7 @@ def run_api_form( response_model = create_model( page_cls.__name__ + "StatusResponse", - __base__=AsyncStatusResponseModelV3[page_cls.ResponseModel], + __base__=AsyncStatusResponseModelV3[response_output_model], ) @app.get( @@ -281,7 +285,7 @@ def get_run_status( status = self.get_run_state(sr.to_dict()) ret |= {"detail": sr.run_status or "", "status": status} if status == RecipeRunState.completed and sr.state: - ret |= {"output": sr.state} + ret |= {"output": sr.api_output()} if sr.retention_policy == RetentionPolicy.delete: sr.state = {} sr.save(update_fields=["state"]) @@ -382,14 +386,14 @@ def submit_api_call( ), ) # create a new run - example_id, run_id, uid = self.create_new_run( + sr = self.create_new_run( enable_rate_limits=enable_rate_limits, is_api_call=True, retention_policy=retention_policy or RetentionPolicy.keep, ) # submit the task - result = self.call_runner_task(example_id, run_id, uid, is_api_call=True) - return self, result, run_id, uid + result = self.call_runner_task(sr.example_id, sr.run_id, sr.uid, is_api_call=True) + return self, result, sr.run_id, sr.uid def build_api_response( @@ -419,20 +423,18 @@ def build_api_response( # wait for the result get_celery_result_db_safe(result) sr = page.run_doc_sr(run_id, uid) - state = sr.to_dict() if sr.retention_policy == RetentionPolicy.delete: sr.state = {} sr.save(update_fields=["state"]) # check for errors - err_msg = state.get(StateKeys.error_msg) - if err_msg: + if sr.error_msg: raise HTTPException( status_code=500, detail={ "id": run_id, "url": web_url, "created_at": sr.created_at.isoformat(), - "error": err_msg, + "error": sr.error_msg, }, ) else: @@ -441,7 +443,7 @@ def build_api_response( "id": run_id, "url": web_url, "created_at": sr.created_at.isoformat(), - "output": state, + "output": sr.api_output(), } diff --git a/scripts/deno-deploy.sh b/scripts/deno-deploy.sh deleted file mode 100755 index d97b110c9..000000000 --- a/scripts/deno-deploy.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -set -ex - -deployctl deploy --include function-executor.js function-executor.js From 1fe9a73e33131a7ec13125931e2aeba20a827b60 Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Sat, 6 Jul 2024 00:29:31 +0530 Subject: [PATCH 04/17] refactor gui runner --- bots/admin.py | 17 ++++------------- celeryapp/tasks.py | 24 +++++++----------------- daras_ai_v2/base.py | 22 +++++++--------------- routers/api.py | 2 +- 4 files changed, 19 insertions(+), 46 deletions(-) diff --git a/bots/admin.py b/bots/admin.py index 20927dbf9..82da0aab2 100644 --- a/bots/admin.py +++ b/bots/admin.py @@ -1,5 +1,6 @@ import datetime import json +from types import SimpleNamespace import django.db.models from django import forms @@ -418,20 +419,10 @@ def view_usage_cost(self, saved_run: SavedRun): def rerun_tasks(self, request, queryset): sr: SavedRun for sr in queryset.all(): - page_cls = Workflow(sr.workflow).page_cls - pr = sr.parent_published_run() - gui_runner.delay( - page_cls=page_cls, - user_id=AppUser.objects.get(uid=sr.uid).id, - run_id=sr.run_id, - uid=sr.uid, - state=sr.to_dict(), - channel=page_cls.realtime_channel_name(sr.run_id, sr.uid), - query_params=page_cls.clean_query_params( - example_id=pr and pr.published_run_id, run_id=sr.run_id, uid=sr.uid - ), - is_api_call=sr.is_api_call, + page = Workflow(sr.workflow).page_cls( + request=SimpleNamespace(user=AppUser.objects.get(uid=sr.uid)) ) + page.call_runner_task(sr) self.message_user( request, f"Started re-running {queryset.count()} tasks in the background.", diff --git a/celeryapp/tasks.py b/celeryapp/tasks.py index eb6dbeb0a..240ade4b3 100644 --- a/celeryapp/tasks.py +++ b/celeryapp/tasks.py @@ -36,31 +36,25 @@ def gui_runner( user_id: str, run_id: str, uid: str, - state: dict, channel: str, - query_params: dict = None, - is_api_call: bool = False, ): - page = page_cls(request=SimpleNamespace(user=AppUser.objects.get(id=user_id))) - def event_processor(event, hint): event["request"] = { "method": "POST", - "url": page.app_url(query_params=query_params), - "data": state, + "url": page.app_url(query_params=st.get_query_params()), + "data": st.session_state, } return event + page = page_cls(request=SimpleNamespace(user=AppUser.objects.get(id=user_id))) page.setup_sentry(event_processor=event_processor) - sr = page.run_doc_sr(run_id, uid) - sr.is_api_call = is_api_call + st.set_session_state(sr.to_dict()) + set_query_params(dict(run_id=run_id, uid=uid)) - st.set_session_state(state) run_time = 0 yield_val = None error_msg = None - set_query_params(query_params or {}) @db_middleware def save(done=False): @@ -118,11 +112,7 @@ def save(done=False): run_time += time() - start_time if isinstance(e, HTTPException) and e.status_code == 402: - error_msg = page.generate_credit_error_message( - example_id=query_params.get("example_id"), - run_id=run_id, - uid=uid, - ) + error_msg = page.generate_credit_error_message(run_id, uid) try: raise UserError(error_msg) from e except UserError as e: @@ -141,7 +131,7 @@ def save(done=False): save() finally: save(done=True) - if not is_api_call: + if not sr.is_api_call: send_email_on_completion(page, sr) run_low_balance_email_check(uid) diff --git a/daras_ai_v2/base.py b/daras_ai_v2/base.py index 57a9774b4..b79968766 100644 --- a/daras_ai_v2/base.py +++ b/daras_ai_v2/base.py @@ -1593,12 +1593,10 @@ def on_submit(self): if settings.CREDITS_TO_DEDUCT_PER_RUN and not self.check_credits(): sr.run_status = "" - sr.error_msg = self.generate_credit_error_message( - sr.example_id, sr.run_id, sr.uid - ) + sr.error_msg = self.generate_credit_error_message(sr.run_id, sr.uid) sr.save(update_fields=["run_status", "error_msg"]) else: - self.call_runner_task(sr.example_id, sr.run_id, sr.uid) + self.call_runner_task(sr) raise RedirectException(self.app_url(run_id=sr.run_id, uid=sr.uid)) @@ -1670,31 +1668,25 @@ def dump_state_to_sr(self, state: dict, sr: SavedRun): } ) - def call_runner_task(self, example_id, run_id, uid, is_api_call=False): + def call_runner_task(self, sr: SavedRun): from celeryapp.tasks import gui_runner return gui_runner.delay( page_cls=self.__class__, user_id=self.request.user.id, - run_id=run_id, - uid=uid, - state=st.session_state, - 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, + run_id=sr.run_id, + uid=sr.uid, + channel=self.realtime_channel_name(sr.run_id, sr.uid), ) @classmethod def realtime_channel_name(cls, run_id, uid): return f"gooey-outputs/{cls.slug_versions[0]}/{uid}/{run_id}" - def generate_credit_error_message(self, example_id, run_id, uid) -> str: + def generate_credit_error_message(self, run_id, uid) -> str: account_url = furl(settings.APP_BASE_URL) / "account/" if self.request.user.is_anonymous: account_url.query.params["next"] = self.app_url( - example_id=example_id, run_id=run_id, uid=uid, query_params={SUBMIT_AFTER_LOGIN_Q: "1"}, diff --git a/routers/api.py b/routers/api.py index dfc96d1cd..8127b1b24 100644 --- a/routers/api.py +++ b/routers/api.py @@ -392,7 +392,7 @@ def submit_api_call( retention_policy=retention_policy or RetentionPolicy.keep, ) # submit the task - result = self.call_runner_task(sr.example_id, sr.run_id, sr.uid, is_api_call=True) + result = self.call_runner_task(sr) return self, result, sr.run_id, sr.uid From b67c8a45587b728dbff6f4c7a1868d79cb89416c Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Sat, 6 Jul 2024 00:44:27 +0530 Subject: [PATCH 05/17] fix bad openai default voice name --- daras_ai_v2/text_to_speech_settings_widgets.py | 11 +++-------- recipes/TextToSpeech.py | 8 ++------ 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/daras_ai_v2/text_to_speech_settings_widgets.py b/daras_ai_v2/text_to_speech_settings_widgets.py index 50dc50b46..2fb29eba0 100644 --- a/daras_ai_v2/text_to_speech_settings_widgets.py +++ b/daras_ai_v2/text_to_speech_settings_widgets.py @@ -7,6 +7,7 @@ import gooey_ui as st from daras_ai_v2 import settings +from daras_ai_v2.custom_enum import GooeyEnum from daras_ai_v2.enum_selector_widget import enum_selector from daras_ai_v2.exceptions import raise_for_status from daras_ai_v2.redis_cache import redis_cache_decorator @@ -28,15 +29,12 @@ } -class OpenAI_TTS_Models(str, Enum): +class OpenAI_TTS_Models(GooeyEnum): tts_1 = "tts-1" tts_1_hd = "tts-1-hd" -OPENAI_TTS_MODELS_T = typing.Literal[tuple(e.name for e in OpenAI_TTS_Models)] - - -class OpenAI_TTS_Voices(str, Enum): +class OpenAI_TTS_Voices(GooeyEnum): alloy = "alloy" echo = "echo" fable = "fable" @@ -45,9 +43,6 @@ class OpenAI_TTS_Voices(str, Enum): shimmer = "shimmer" -OPENAI_TTS_VOICES_T = typing.Literal[tuple(e.name for e in OpenAI_TTS_Voices)] - - class TextToSpeechProviders(Enum): GOOGLE_TTS = "Google Text-to-Speech" ELEVEN_LABS = "Eleven Labs" diff --git a/recipes/TextToSpeech.py b/recipes/TextToSpeech.py index 9d5e1eebf..f1e5449ce 100644 --- a/recipes/TextToSpeech.py +++ b/recipes/TextToSpeech.py @@ -21,8 +21,6 @@ TextToSpeechProviders, text_to_speech_provider_selector, azure_tts_voices, - OPENAI_TTS_MODELS_T, - OPENAI_TTS_VOICES_T, OpenAI_TTS_Models, OpenAI_TTS_Voices, OLD_ELEVEN_LABS_VOICES, @@ -56,8 +54,8 @@ class TextToSpeechSettings(BaseModel): azure_voice_name: str | None - openai_voice_name: OPENAI_TTS_VOICES_T | None - openai_tts_model: OPENAI_TTS_MODELS_T | None + openai_voice_name: OpenAI_TTS_Voices.api_choices | None + openai_tts_model: OpenAI_TTS_Models.api_choices | None class TextToSpeechPage(BasePage): @@ -81,8 +79,6 @@ class TextToSpeechPage(BasePage): "elevenlabs_model": "eleven_multilingual_v2", "elevenlabs_stability": 0.5, "elevenlabs_similarity_boost": 0.75, - "openai_voice_name": "alloy", - "openai_tts_model": "tts-1", } class RequestModelBase(BasePage.RequestModel): From 4a0f1f558754aa2741eccfee382974a27bdf4b81 Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Sat, 6 Jul 2024 00:54:37 +0530 Subject: [PATCH 06/17] fix none type error --- recipes/CompareUpscaler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/recipes/CompareUpscaler.py b/recipes/CompareUpscaler.py index 875280883..55f6c4a0f 100644 --- a/recipes/CompareUpscaler.py +++ b/recipes/CompareUpscaler.py @@ -183,10 +183,10 @@ def related_workflows(self) -> list: def _render_outputs(state): for key in state.get("selected_models") or []: - img = state.get("output_images", {}).get(key) + img = (state.get("output_images") or {}).get(key) if img: st.image(img, caption=UpscalerModels[key].label, show_download_button=True) - vid = state.get("output_videos", {}).get(key) + vid = (state.get("output_videos") or {}).get(key) if vid: st.video(vid, caption=UpscalerModels[key].label, show_download_button=True) From bb2b75ac18c1ab92f9cb5d0e22af4914fb785909 Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Sat, 6 Jul 2024 01:21:17 +0530 Subject: [PATCH 07/17] fix parent published run not set when calling submit_api_call() --- bots/models.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/bots/models.py b/bots/models.py index a2c5715a1..02512dd2a 100644 --- a/bots/models.py +++ b/bots/models.py @@ -347,18 +347,23 @@ def submit_api_call( # run in a thread to avoid messing up threadlocals with ThreadPool(1) as pool: + pr = self.parent_published_run() + query_params = dict( + example_id=(pr and pr.published_run_id) or self.example_id, + run_id=self.run_id, + uid=self.uid, + ) page, result, run_id, uid = pool.apply( submit_api_call, kwds=dict( page_cls=Workflow(self.workflow).page_cls, - query_params=dict( - example_id=self.example_id, run_id=self.run_id, uid=self.uid - ), + query_params=query_params, user=current_user, request_body=request_body, enable_rate_limits=enable_rate_limits, ), ) + return result, page.run_doc_sr(run_id, uid) def get_creator(self) -> AppUser | None: From e2b21a0ee21e3405b74113720cacdba73c46d140 Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Sat, 6 Jul 2024 01:46:31 +0530 Subject: [PATCH 08/17] fix parent published run not set when calling submit_api_call() --- bots/models.py | 28 ++++++++++++++++++++++------ bots/tasks.py | 4 +++- daras_ai_v2/safety_checker.py | 2 +- functions/recipe_functions.py | 1 + recipes/BulkRunner.py | 6 ++++-- 5 files changed, 31 insertions(+), 10 deletions(-) diff --git a/bots/models.py b/bots/models.py index 02512dd2a..6ff6674ff 100644 --- a/bots/models.py +++ b/bots/models.py @@ -342,17 +342,19 @@ def submit_api_call( current_user: AppUser, request_body: dict, enable_rate_limits: bool = False, + parent_pr: "PublishedRun" = None, ) -> tuple["celery.result.AsyncResult", "SavedRun"]: from routers.api import submit_api_call # run in a thread to avoid messing up threadlocals with ThreadPool(1) as pool: - pr = self.parent_published_run() - query_params = dict( - example_id=(pr and pr.published_run_id) or self.example_id, - run_id=self.run_id, - uid=self.uid, - ) + if parent_pr and parent_pr.saved_run == self: + # avoid passing run_id and uid for examples + query_params = dict(example_id=parent_pr.published_run_id) + else: + query_params = dict( + example_id=self.example_id, run_id=self.run_id, uid=self.uid + ) page, result, run_id, uid = pool.apply( submit_api_call, kwds=dict( @@ -1678,6 +1680,20 @@ def get_run_count(self): or 0 ) + def submit_api_call( + self, + *, + current_user: AppUser, + request_body: dict, + enable_rate_limits: bool = False, + ) -> tuple["celery.result.AsyncResult", "SavedRun"]: + return self.saved_run.submit_api_call( + current_user=current_user, + request_body=request_body, + enable_rate_limits=enable_rate_limits, + parent_pr=self, + ) + class PublishedRunVersion(models.Model): version_id = models.CharField(max_length=128, unique=True) diff --git a/bots/tasks.py b/bots/tasks.py index aca2ec369..d49abd6bc 100644 --- a/bots/tasks.py +++ b/bots/tasks.py @@ -92,7 +92,9 @@ def msg_analysis(self, msg_id: int, anal_id: int, countdown: int | None): # make the api call result, sr = analysis_sr.submit_api_call( - current_user=billing_account, request_body=dict(variables=variables) + current_user=billing_account, + request_body=dict(variables=variables), + parent_pr=anal.published_run, ) # save the run before the result is ready diff --git a/daras_ai_v2/safety_checker.py b/daras_ai_v2/safety_checker.py index 8ae962cc4..841817d94 100644 --- a/daras_ai_v2/safety_checker.py +++ b/daras_ai_v2/safety_checker.py @@ -30,7 +30,7 @@ def safety_checker_text(text_input: str): result, sr = ( CompareLLMPage() .get_published_run(published_run_id=settings.SAFTY_CHECKER_EXAMPLE_ID) - .saved_run.submit_api_call( + .submit_api_call( current_user=billing_account, request_body=dict(variables=dict(input=text_input)), ) diff --git a/functions/recipe_functions.py b/functions/recipe_functions.py index ff3c2f734..ae34ca54f 100644 --- a/functions/recipe_functions.py +++ b/functions/recipe_functions.py @@ -44,6 +44,7 @@ def call_recipe_functions( page_cls, sr, pr = url_to_runs(fun.url) result, sr = sr.submit_api_call( current_user=current_user, + parent_pr=pr, request_body=dict( variables=sr.state.get("variables", {}) | variables diff --git a/recipes/BulkRunner.py b/recipes/BulkRunner.py index 6fb6dd5d9..87b218954 100644 --- a/recipes/BulkRunner.py +++ b/recipes/BulkRunner.py @@ -318,7 +318,9 @@ def run_v2( yield f"{progress}%" result, sr = sr.submit_api_call( - current_user=self.request.user, request_body=request_body + current_user=self.request.user, + request_body=request_body, + parent_pr=pr, ) get_celery_result_db_safe(result) sr.refresh_from_db() @@ -388,7 +390,7 @@ def run_v2( documents=response.output_documents ).dict(exclude_unset=True) result, sr = sr.submit_api_call( - current_user=self.request.user, request_body=request_body + current_user=self.request.user, request_body=request_body, parent_pr=pr ) get_celery_result_db_safe(result) sr.refresh_from_db() From 142d8956af6e975c92aead8f243029730f13cfcc Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Sat, 6 Jul 2024 20:45:16 +0530 Subject: [PATCH 09/17] handle validation error from create_new_run --- daras_ai_v2/base.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/daras_ai_v2/base.py b/daras_ai_v2/base.py index b79968766..fba67cbc0 100644 --- a/daras_ai_v2/base.py +++ b/daras_ai_v2/base.py @@ -19,7 +19,7 @@ from fastapi import HTTPException from firebase_admin import auth from furl import furl -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, ValidationError from sentry_sdk.tracing import ( TRANSACTION_SOURCE_ROUTE, ) @@ -1583,6 +1583,10 @@ def on_submit(self): try: sr = self.create_new_run(enable_rate_limits=True) + except ValidationError as e: + st.session_state[StateKeys.run_status] = None + st.session_state[StateKeys.error_msg] = str(e) + return except RateLimitExceeded as e: st.session_state[StateKeys.run_status] = None st.session_state[StateKeys.error_msg] = e.detail.get("error", "") From b14b934bfa695f9b1f61a472882c111658e30c83 Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Sun, 7 Jul 2024 17:21:36 +0530 Subject: [PATCH 10/17] update yt-dlp --- poetry.lock | 38 +++++++++++++++++++++++++------------- pyproject.toml | 2 +- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/poetry.lock b/poetry.lock index a9a6f950a..70ce360c0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4741,13 +4741,13 @@ dev = ["mypy", "pylint", "pytest", "pytest-asyncio", "pytest-recording", "respx" [[package]] name = "requests" -version = "2.31.0" +version = "2.32.3" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, - {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] [package.dependencies] @@ -6391,22 +6391,34 @@ multidict = ">=4.0" [[package]] name = "yt-dlp" -version = "2023.10.13" -description = "A youtube-dl fork with additional features and patches" +version = "2024.7.2" +description = "A feature-rich command-line audio/video downloader" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "yt-dlp-2023.10.13.tar.gz", hash = "sha256:e026ea1c435ff36eef1215bc4c5bb8c479938b90054997ba99f63a4541fe63b4"}, - {file = "yt_dlp-2023.10.13-py2.py3-none-any.whl", hash = "sha256:2b069f22675532eebacdfd6372b1825651a751fef848de9ae6efe6491b2dc38a"}, + {file = "yt_dlp-2024.7.2-py3-none-any.whl", hash = "sha256:4f76b48244c783e6ac06e8d7627bcf62cbeb4f6d79ba7e3cfc8249e680d4e691"}, + {file = "yt_dlp-2024.7.2.tar.gz", hash = "sha256:2b0c86b579d4a044eaf3c4b00e3d7b24d82e6e26869fa11c288ea4395b387f41"}, ] [package.dependencies] -brotli = {version = "*", markers = "platform_python_implementation == \"CPython\""} -brotlicffi = {version = "*", markers = "platform_python_implementation != \"CPython\""} +brotli = {version = "*", markers = "implementation_name == \"cpython\""} +brotlicffi = {version = "*", markers = "implementation_name != \"cpython\""} certifi = "*" mutagen = "*" pycryptodomex = "*" -websockets = "*" +requests = ">=2.32.2,<3" +urllib3 = ">=1.26.17,<3" +websockets = ">=12.0" + +[package.extras] +build = ["build", "hatchling", "pip", "setuptools", "wheel"] +curl-cffi = ["curl-cffi (==0.5.10)"] +dev = ["autopep8 (>=2.0,<3.0)", "pre-commit", "pytest (>=8.1,<9.0)", "ruff (>=0.5.0,<0.6.0)"] +py2exe = ["py2exe (>=0.12)"] +pyinstaller = ["pyinstaller (>=6.7.0)"] +secretstorage = ["cffi", "secretstorage"] +static-analysis = ["autopep8 (>=2.0,<3.0)", "ruff (>=0.5.0,<0.6.0)"] +test = ["pytest (>=8.1,<9.0)"] [[package]] name = "zipp" @@ -6426,4 +6438,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "51cec8cd0df55b484b5d2d36bc08ea69104896c7b043dac50a054f0b5c89645f" +content-hash = "fa29696b70dce0df83af18fb12a0f311dd05dccabfc7d3fe9ffe71630e042f14" diff --git a/pyproject.toml b/pyproject.toml index d0b85af5b..d703612fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ oauth2client = "^4.1.3" tiktoken = "^0.7.0" google-cloud-translate = "^3.12.0" google-cloud-speech = "^2.21.0" -yt-dlp = "^2023.3.4" +yt-dlp = "^2024.7.2" nltk = "^3.8.1" Jinja2 = "^3.1.2" Django = "^4.2" From fd80e78841226d705ba6dafeb979b8aab8d2295d Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Sun, 7 Jul 2024 17:27:36 +0530 Subject: [PATCH 11/17] celery loglevel info --- scripts/run-prod.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/run-prod.sh b/scripts/run-prod.sh index cafd555fd..04c419d83 100755 --- a/scripts/run-prod.sh +++ b/scripts/run-prod.sh @@ -14,7 +14,7 @@ elif [ "$RUN_DJANGO" ]; then elif [ "$RUN_STREAMLIT" ]; then SENTRY_ENVIRONMENT="streamlit" exec streamlit run Home.py --server.address=0.0.0.0 --server.port=8000 elif [ "$RUN_CELERY" ]; then - SENTRY_ENVIRONMENT="celery" exec celery -A celeryapp worker -P prefork -c ${MAX_THREADS:-1} --max-tasks-per-child 1 + SENTRY_ENVIRONMENT="celery" exec celery -A celeryapp worker -l INFO -P prefork -c ${MAX_THREADS:-1} --max-tasks-per-child 1 else SENTRY_ENVIRONMENT="fastapi" exec uvicorn server:app --host 0.0.0.0 --port 8000 fi From 69b4a814ae030ad3d0101f0761a586f7d6b683df Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Mon, 8 Jul 2024 00:38:17 +0530 Subject: [PATCH 12/17] perf optimization on startup time for celery tasks report complete run time of celery task to frontend, not just the steps fix sentry urls --- celeryapp/tasks.py | 130 +++++++++++--------------- daras_ai_v2/base.py | 42 ++++++--- tests/test_low_balance_email_check.py | 16 ++-- 3 files changed, 89 insertions(+), 99 deletions(-) diff --git a/celeryapp/tasks.py b/celeryapp/tasks.py index 240ade4b3..b2a7b4327 100644 --- a/celeryapp/tasks.py +++ b/celeryapp/tasks.py @@ -26,114 +26,93 @@ from daras_ai_v2.settings import templates from gooey_ui.pubsub import realtime_push from gooey_ui.state import set_query_params -from gooeysite.bg_db_conn import db_middleware, next_db_safe +from gooeysite.bg_db_conn import db_middleware + +DEFAULT_RUN_STATUS = "Running..." @app.task def gui_runner( *, page_cls: typing.Type[BasePage], - user_id: str, + user_id: int, run_id: str, uid: str, channel: str, ): - def event_processor(event, hint): - event["request"] = { - "method": "POST", - "url": page.app_url(query_params=st.get_query_params()), - "data": st.session_state, - } - return event - - page = page_cls(request=SimpleNamespace(user=AppUser.objects.get(id=user_id))) - page.setup_sentry(event_processor=event_processor) - sr = page.run_doc_sr(run_id, uid) - st.set_session_state(sr.to_dict()) - set_query_params(dict(run_id=run_id, uid=uid)) - - run_time = 0 - yield_val = None + start_time = time() error_msg = None @db_middleware - def save(done=False): + def save_on_step(yield_val: str | tuple[str, dict] = None, *, done: bool = False): + if isinstance(yield_val, tuple): + run_status, extra_output = yield_val + else: + run_status = yield_val + extra_output = {} + if done: - # clear run status run_status = None else: - # set run status to the yield value of generator - run_status = yield_val or "Running..." - if isinstance(run_status, tuple): - run_status, extra_output = run_status - else: - extra_output = {} - # set run status and run time - status = { - StateKeys.run_time: run_time, - StateKeys.error_msg: error_msg, - StateKeys.run_status: run_status, - } + run_status = run_status or DEFAULT_RUN_STATUS + output = ( - status - | - # extract outputs from local state + # extract status of the run { + StateKeys.error_msg: error_msg, + StateKeys.run_time: time() - start_time, + StateKeys.run_status: run_status, + } + # extract outputs from local state + | { k: v for k, v in st.session_state.items() if k in page.ResponseModel.__fields__ } + # add extra outputs from the run | extra_output ) + # send outputs to ui realtime_push(channel, output) # save to db page.dump_state_to_sr(st.session_state | output, sr) + user = AppUser.objects.get(id=user_id) + page = page_cls(request=SimpleNamespace(user=user)) + page.setup_sentry() + sr = page.run_doc_sr(run_id, uid) + st.set_session_state(sr.to_dict()) + set_query_params(dict(run_id=run_id, uid=uid)) + try: - gen = page.main(sr, st.session_state) - save() - while True: - # record time - start_time = time() + save_on_step() + for val in page.main(sr, st.session_state): + save_on_step(val) + # render errors nicely + except Exception as e: + if isinstance(e, HTTPException) and e.status_code == 402: + error_msg = page.generate_credit_error_message(run_id, uid) try: - # advance the generator (to further progress of run()) - yield_val = next_db_safe(gen) - # increment total time taken after every iteration - run_time += time() - start_time - continue - # run completed - except StopIteration: - run_time += time() - start_time - sr.transaction, sr.price = page.deduct_credits(st.session_state) - break - # render errors nicely - except Exception as e: - run_time += time() - start_time - - if isinstance(e, HTTPException) and e.status_code == 402: - error_msg = page.generate_credit_error_message(run_id, uid) - try: - raise UserError(error_msg) from e - except UserError as e: - sentry_sdk.capture_exception(e, level=e.sentry_level) - break - - if isinstance(e, UserError): - sentry_level = e.sentry_level - else: - sentry_level = "error" - traceback.print_exc() - sentry_sdk.capture_exception(e, level=sentry_level) - error_msg = err_msg_for_exc(e) - break - finally: - save() + raise UserError(error_msg) from e + except UserError as e: + sentry_sdk.capture_exception(e, level=e.sentry_level) + else: + if isinstance(e, UserError): + sentry_level = e.sentry_level + else: + sentry_level = "error" + traceback.print_exc() + sentry_sdk.capture_exception(e, level=sentry_level) + error_msg = err_msg_for_exc(e) + # run completed successfully, deduct credits + else: + sr.transaction, sr.price = page.deduct_credits(st.session_state) finally: - save(done=True) + save_on_step(done=True) if not sr.is_api_call: send_email_on_completion(page, sr) - run_low_balance_email_check(uid) + run_low_balance_email_check(user) def err_msg_for_exc(e: Exception): @@ -162,11 +141,10 @@ def err_msg_for_exc(e: Exception): return f"{type(e).__name__}: {e}" -def run_low_balance_email_check(uid: str): +def run_low_balance_email_check(user: AppUser): # don't send email if feature is disabled if not settings.LOW_BALANCE_EMAIL_ENABLED: return - user = AppUser.objects.get(uid=uid) # don't send email if user is not paying or has enough balance if not user.is_paying or user.balance > settings.LOW_BALANCE_EMAIL_CREDITS: return diff --git a/daras_ai_v2/base.py b/daras_ai_v2/base.py index fba67cbc0..b1324fb68 100644 --- a/daras_ai_v2/base.py +++ b/daras_ai_v2/base.py @@ -262,11 +262,31 @@ def clean_query_params(cls, *, example_id, run_id, uid) -> dict: def get_dynamic_meta_title(self) -> str | None: return None - def setup_sentry(self, event_processor: typing.Callable = None): - def add_user_to_event(event, hint): - user = self.request and self.request.user - if not user: - return event + def setup_sentry(self): + with sentry_sdk.configure_scope() as scope: + scope.set_transaction_name( + "/" + self.slug_versions[0], source=TRANSACTION_SOURCE_ROUTE + ) + scope.add_event_processor(self.sentry_event_set_request) + scope.add_event_processor(self.sentry_event_set_user) + + def sentry_event_set_request(self, event, hint): + request = event.setdefault("request", {}) + request.setdefault("method", "POST") + request["data"] = st.session_state + if url := request.get("url"): + f = furl(url) + request["url"] = str( + furl(settings.APP_BASE_URL, path=f.pathstr, query=f.querystr).url + ) + else: + request["url"] = self.app_url( + tab=self.tab, query_params=st.get_query_params() + ) + return event + + def sentry_event_set_user(self, event, hint): + if user := self.request and self.request.user: event["user"] = { "id": user.id, "name": user.display_name, @@ -282,20 +302,12 @@ def add_user_to_event(event, hint): "is_anonymous", "is_disabled", "disable_safety_checker", + "disable_rate_limits", "created_at", ] }, } - return event - - with sentry_sdk.configure_scope() as scope: - scope.set_extra("base_url", self.app_url()) - scope.set_transaction_name( - "/" + self.slug_versions[0], source=TRANSACTION_SOURCE_ROUTE - ) - scope.add_event_processor(add_user_to_event) - if event_processor: - scope.add_event_processor(event_processor) + return event def refresh_state(self): _, run_id, uid = extract_query_params(gooey_get_query_params()) diff --git a/tests/test_low_balance_email_check.py b/tests/test_low_balance_email_check.py index 66203a19b..dc9c74148 100644 --- a/tests/test_low_balance_email_check.py +++ b/tests/test_low_balance_email_check.py @@ -12,7 +12,7 @@ def test_dont_send_email_if_feature_is_disabled(transactional_db): uid="test_user", is_paying=True, balance=0, is_anonymous=False ) settings.LOW_BALANCE_EMAIL_ENABLED = False - run_low_balance_email_check(user.uid) + run_low_balance_email_check(user) assert not pytest_outbox @@ -21,7 +21,7 @@ def test_dont_send_email_if_user_is_not_paying(transactional_db): uid="test_user", is_paying=False, balance=0, is_anonymous=False ) settings.LOW_BALANCE_EMAIL_ENABLED = True - run_low_balance_email_check(user.uid) + run_low_balance_email_check(user) assert not pytest_outbox @@ -31,7 +31,7 @@ def test_dont_send_email_if_user_has_enough_balance(transactional_db): ) settings.LOW_BALANCE_EMAIL_CREDITS = 100 settings.LOW_BALANCE_EMAIL_ENABLED = True - run_low_balance_email_check(user.uid) + run_low_balance_email_check(user) assert not pytest_outbox @@ -46,7 +46,7 @@ def test_dont_send_email_if_user_has_been_emailed_recently(transactional_db): settings.LOW_BALANCE_EMAIL_ENABLED = True settings.LOW_BALANCE_EMAIL_DAYS = 1 settings.LOW_BALANCE_EMAIL_CREDITS = 100 - run_low_balance_email_check(user.uid) + run_low_balance_email_check(user) assert not pytest_outbox @@ -77,14 +77,14 @@ def test_send_email_if_user_has_been_email_recently_but_made_a_purchase( settings.LOW_BALANCE_EMAIL_ENABLED = True settings.LOW_BALANCE_EMAIL_DAYS = 1 settings.LOW_BALANCE_EMAIL_CREDITS = 100 - run_low_balance_email_check(user.uid) + run_low_balance_email_check(user) assert len(pytest_outbox) == 1 assert " 22" in pytest_outbox[0]["html_body"] assert " 78" in pytest_outbox[0]["html_body"] pytest_outbox.clear() - run_low_balance_email_check(user.uid) + run_low_balance_email_check(user) assert not pytest_outbox @@ -114,7 +114,7 @@ def test_send_email(transactional_db): settings.LOW_BALANCE_EMAIL_DAYS = 1 settings.LOW_BALANCE_EMAIL_CREDITS = 100 - run_low_balance_email_check(user.uid) + run_low_balance_email_check(user) assert len(pytest_outbox) == 1 body = pytest_outbox[0]["html_body"] assert " 66" in body @@ -123,5 +123,5 @@ def test_send_email(transactional_db): assert " 100" not in body pytest_outbox.clear() - run_low_balance_email_check(user.uid) + run_low_balance_email_check(user) assert not pytest_outbox From 8733b626acaa83c7b7db2683ac37c47495573a88 Mon Sep 17 00:00:00 2001 From: anish-work Date: Thu, 7 Mar 2024 19:11:26 +0530 Subject: [PATCH 13/17] make components page --- components_page.py | 363 +++++++++++++++++++++++++++++++++++++++++++++ routers/root.py | 22 +++ 2 files changed, 385 insertions(+) create mode 100644 components_page.py diff --git a/components_page.py b/components_page.py new file mode 100644 index 000000000..f36b1784c --- /dev/null +++ b/components_page.py @@ -0,0 +1,363 @@ +import gooey_ui as gui +from gooey_ui import state + +META_TITLE = "Gooey Components" +META_DESCRIPTION = "Explore the Gooey Component Library" + +TITLE = "Gooey AI - Component Library" +DESCRIPTION = "See & Learn for yourself" + + +def render(): + heading(title=TITLE, description=DESCRIPTION) + + with gui.tag( + "div", + className="mt-4 container-fluid", + ): + render_layouts() + render_content() + render_components() + + +def render_layouts(): + section_title("Layouts") + sub_section_title("Full Width Layout") + with gui.tag("div", className="container-fluid bg-light p-4"): + with gui.tag("div", className="row"): + with gui.tag("div", className="col-12"): + gui.html("This is a full width layout") + code_block( + """ + with gui.tag("div", className="container-fluid bg-light p-4"): + with gui.tag("div", className="row"): + with gui.tag("div", className="col-12"): + gui.html("This is a full width layout")""" + ) + sub_section_title("Full Width 1/2 Layout") + with gui.tag("div", className="container-fluid p-2"): + with gui.tag("div", className="row"): + with gui.tag("div", className="col-6 border"): + gui.html("This is a 1/2 width layout") + with gui.tag("div", className="col-6 border"): + gui.html("This is a 1/2 width layout") + code_block( + """with gui.tag("div", className="container-fluid p-4"): + with gui.tag("div", className="row"): + with gui.tag("div", className="col-6 border"): + gui.html("This is a 1/2 width layout") + with gui.tag("div", className="col-6 border"): + gui.html("This is a 1/2 width layout")""" + ) + sub_section_title("Full Width 1/3 Layout") + with gui.tag("div", className="container-fluid p-2"): + with gui.tag("div", className="row"): + with gui.tag("div", className="col-4 border"): + gui.html("This is a 1/3 width layout") + with gui.tag("div", className="col-4 border"): + gui.html("This is a 1/3 width layout") + with gui.tag("div", className="col-4 border"): + gui.html("This is a 1/3 width layout") + code_block( + """with gui.tag("div", className="container-fluid p-2"): + with gui.tag("div", className="row"): + with gui.tag("div", className="col-4 border"): + gui.html("This is a 1/3 width layout") + with gui.tag("div", className="col-4 border"): + gui.html("This is a 1/3 width layout") + with gui.tag("div", className="col-4 border"): + gui.html("This is a 1/3 width layout")""" + ) + sub_section_title("Responsive 1/3 Layout") + gui.write("These columns will go full width on small devices") + with gui.tag("div", className="container-fluid p-2"): + with gui.tag("div", className="row"): + with gui.tag("div", className="col-12 col-md-4 border"): + gui.html("This is a responsive 1/3 width layout") + with gui.tag("div", className="col-12 col-md-4 border"): + gui.html("This is a responsive 1/3 width layout") + with gui.tag("div", className="col-12 col-md-4 border"): + gui.html("This is a responsive 1/3 width layout") + code_block( + """with gui.tag("div", className="container-fluid p-2"): + with gui.tag("div", className="row"): + with gui.tag("div", className="col-12 col-md-4 border"): + gui.html("This is a responsive 1/3 width layout") + with gui.tag("div", className="col-12 col-md-4 border"): + gui.html("This is a responsive 1/3 width layout") + with gui.tag("div", className="col-12 col-md-4 border"): + gui.html("This is a responsive 1/3 width layout") + """ + ) + + +def render_content(): + section_title("Content") + with gui.tag("div", className="container-fluid"): + with gui.tag("div", className="row"): + # LEFT SIDE + with gui.tag("div", className="col-12 col-md-6"): + render_headings() + + # RIGHT SIDE + with gui.tag("div", className="col-12 col-md-6"): + sub_section_title("Normal Text") + gui.write("This is a normal text") + code_block('gui.write("This is a normal text")') + sub_section_title("Link") + with gui.tag("a", href="https://www.gooey.ai"): + gui.html("This is a link") + code_block( + """with gui.tag("a", href="https://www.gooey.ai"): + gui.html("This is a link")""" + ) + sub_section_title("Colored Text") + with gui.tag("p", className="text-primary"): + gui.html("This is a primary text") + with gui.tag("p", className="text-secondary"): + gui.html("This is a secondary text") + with gui.tag("p", className="text-success"): + gui.html("This is a success text") + with gui.tag("p", className="text-danger"): + gui.html("This is a danger text") + with gui.tag("p", className="text-warning"): + gui.html("This is a warning text") + with gui.tag("p", className="text-info"): + gui.html("This is a info text") + with gui.tag("p", className="text-light bg-dark"): + gui.html("This is a light text") + code_block( + """with gui.tag("p", className="text-primary"): + gui.html("This is a primary text") + with gui.tag("p", className="text-secondary"): + gui.html("This is a secondary text") + with gui.tag("p", className="text-success"): + gui.html("This is a success text") + with gui.tag("p", className="text-danger"): + gui.html("This is a danger text") + with gui.tag("p", className="text-warning"): + gui.html("This is a warning text") + with gui.tag("p", className="text-info"): + gui.html("This is a info text") + with gui.tag("p", className="text-light bg-dark"): + gui.html("This is a light text")""" + ) + + +def render_components(): + section_title("Components") + with gui.tag("div", className="container-fluid"): + with gui.tag("div", className="row"): + # LEFT SIDE + with gui.tag("div", className="col-12 col-md-6"): + # BUTTONS + buttons_group() + + # TABS + render_tabs_example() + + # RIGHT SIDE + with gui.tag("div", className="col-12 col-md-6"): + # ALERTS + render_alerts() + + # Inputs + render_inputs() + section_title("File Upload Button") + file = gui.file_uploader( + "**Upload Any File**", + key="file_uploader_test0", + help="Attach a video/audio/file to this.", + optional=True, + accept=["audio/*"], + ) + + +def render_headings(): + sub_section_title("Headings") + with gui.tag("h1"): + gui.html("This is a h1 heading") + with gui.tag("h2"): + gui.html("This is a h2 heading") + with gui.tag("h3"): + gui.html("This is a h3 heading") + with gui.tag("h4"): + gui.html("This is a h4 heading") + with gui.tag("h5"): + gui.html("This is a h5 heading") + with gui.tag("h6"): + gui.html("This is a h6 heading") + + code_block( + """ + with gui.tag("h1"): + gui.html("This is a h1 heading") + with gui.tag("h2"): + gui.html("This is a h2 heading") + with gui.tag("h3"): + gui.html("This is a h3 heading") + with gui.tag("h4"): + gui.html("This is a h4 heading") + with gui.tag("h5"): + gui.html("This is a h5 heading") + with gui.tag("h6"): + gui.html("This is a h6 heading") + """ + ) + + +def render_inputs(): + section_title("Inputs") + sub_section_title("Text Area") + gui.text_area( + "Label: Take some input from user", + "", + 100, + "textArea_test0", + "want help?", + "You can also show a placeholder", + ) + code_block( + 'gui.text_area("Example Text Area","",100,"textAreat_test0","want help?","You can also show a placeholder")' + ) + + sub_section_title("Multi Select") + gui.multiselect( + "Label: Click below to select multiple options", + ["Option 1", "Option 2", "Option 3", "Option 4"], + ) + code_block( + 'gui.multiselect("Multi Select", ["Option 1", "Option 2", "Option 3", "Option 4"])' + ) + + +def render_alerts(): + section_title("Alerts") + gui.success("Yay, this is done!") + code_block('gui.success("Yay, this is done!")') + + gui.error("Opps, please try again!") + code_block('gui.error("Opps, please try again!")') + + +def render_tabs_example(): + section_title("Tabs") + # Rounded Tabs + sub_section_title("Rounded Tabs") + tab1, tab2, tab3 = gui.tabs(["Tab 1", "Tab 2", "Tab 3"]) + with tab1: + gui.write("This is tab 1 content") + with tab2: + gui.write("This is tab 2 content") + with tab3: + gui.write("This is tab 3 content") + + code_block( + """ + tab1, tab2, tab3 = gui.tabs(["Tab 1", "Tab 2", "Tab 3"]) + with tab1: + gui.html("This is tab 1 content") + with tab2: + gui.html("This is tab 2 content") + with tab3: + gui.html("This is tab 3 content") + """ + ) + + # Underline Tabs + selected_tab = "Tab 1" + sub_section_title("Underline Tabs") + with gui.nav_tabs(): + for name in ["Tab 1", "Tab 2", "Tab 4"]: + url = "/components" + with gui.nav_item(url, active=name == selected_tab): + gui.html(name) + with gui.nav_tab_content(): + if selected_tab == "Tab 1": + gui.write("This is tab 1 content") + elif selected_tab == "Tab 2": + gui.write("This is tab 2 content") + else: + gui.write("This is tab 3 content") + + code_block( + """ + with gui.nav_tabs(): + for name in ["Tab 1", "Tab 2", "Tab 4"]: + url = "/components" + with gui.nav_item(url, active=name == selected_tab): + gui.html(name) + with gui.nav_tab_content(): + if selected_tab == "Tab 1": + gui.write("This is tab 1 content") + elif selected_tab == "Tab 2": + gui.write("This is tab 2 content") + else: + gui.write("This is tab 3 content") + """ + ) + + +def buttons_group(): + sub_section_title("Buttons") + with gui.tag("div"): + with gui.tag("div", className="d-flex justify-content-around"): + gui.button("Primary", key="test0", type="primary") + gui.button("Secondary", key="test1") + gui.button("Tertiary", key="test3", type="tertiary") + gui.button("Link Button", key="test3", type="link") + + code_block('gui.button("Primary", key="test0", type="primary")') + code_block('gui.button("Secondary", key="test1")') + code_block('gui.button("Tertiary", key="test3", type="tertiary")') + code_block('gui.button("Link Button", key="test3", type="link")') + + +def code_block(content: str): + with gui.tag("div", className="mt-4"): + gui.write( + rf""" + ```python + %s + """ + % content, + unsafe_allow_html=True, + ) + + +def collapsible_section(title: str) -> state.NestingCtx: + with gui.expander(title): + return state.NestingCtx() + + +def section_title(title: str): + with gui.tag("div", className="mb-4 mt-4 bg-light border-1 p-2"): + with gui.tag("h3", style={"font-weight": "500", "margin": "0"}): + gui.html(title.upper()) + + +def sub_section_title(title: str): + with gui.tag( + "h4", + className="text-muted mb-2", + ): + gui.html(title) + + +def heading( + title: str, description: str, margin_top: str = "2rem", margin_bottom: str = "2rem" +): + with gui.tag( + "div", style={"margin-top": margin_top, "margin-bottom": margin_bottom} + ): + with gui.tag( + "p", + style={"margin-top": "0rem", "margin-bottom": "0rem"}, + className="text-muted", + ): + gui.html(description.upper()) + with gui.tag( + "h1", + style={"margin-top": "0px", "margin-bottom": "0px", "font-weight": "500"}, + ): + gui.html(title) diff --git a/routers/root.py b/routers/root.py index dfc784b0e..e9d6bca80 100644 --- a/routers/root.py +++ b/routers/root.py @@ -208,6 +208,28 @@ def file_upload(form_data: FormData = fastapi_request_form): return {"url": upload_file_from_bytes(filename, data, content_type)} +async def request_json(request: Request): + return await request.json() + + +@app.post("/components/") +def component_page(request: Request, json_data: dict = Depends(request_json)): + import components_page + + ret = st.runner( + lambda: page_wrapper(request=request, render_fn=components_page.render), + **json_data, + ) + ret |= { + "meta": raw_build_meta_tags( + url=get_og_url_path(request), + title=components_page.META_TITLE, + description=components_page.META_DESCRIPTION, + ), + } + return ret + + @app.post("/explore/") @st.route def explore_page(request: Request): From 84c7e8b0cec5e884f3f893a790ad84b67f35d575 Mon Sep 17 00:00:00 2001 From: anish-work Date: Wed, 15 May 2024 04:29:31 +0530 Subject: [PATCH 14/17] update components page to support new page_wrapper --- components_page.py => components_doc.py | 0 routers/root.py | 19 +++++++++---------- 2 files changed, 9 insertions(+), 10 deletions(-) rename components_page.py => components_doc.py (100%) diff --git a/components_page.py b/components_doc.py similarity index 100% rename from components_page.py rename to components_doc.py diff --git a/routers/root.py b/routers/root.py index e9d6bca80..ade410607 100644 --- a/routers/root.py +++ b/routers/root.py @@ -213,21 +213,20 @@ async def request_json(request: Request): @app.post("/components/") -def component_page(request: Request, json_data: dict = Depends(request_json)): - import components_page +@st.route +def component_page(request: Request): + import components_doc - ret = st.runner( - lambda: page_wrapper(request=request, render_fn=components_page.render), - **json_data, - ) - ret |= { + with page_wrapper(request): + components_doc.render() + + return { "meta": raw_build_meta_tags( url=get_og_url_path(request), - title=components_page.META_TITLE, - description=components_page.META_DESCRIPTION, + title=components_doc.META_TITLE, + description=components_doc.META_DESCRIPTION, ), } - return ret @app.post("/explore/") From fec3a4d4e7af2c71e0ca8d0a5b379cab52febff9 Mon Sep 17 00:00:00 2001 From: anish-work Date: Thu, 20 Jun 2024 18:34:45 +0530 Subject: [PATCH 15/17] use inspect.getsource() --- components_doc.py | 419 ++++++++++++++++++++++------------------------ 1 file changed, 196 insertions(+), 223 deletions(-) diff --git a/components_doc.py b/components_doc.py index f36b1784c..569ae80af 100644 --- a/components_doc.py +++ b/components_doc.py @@ -1,5 +1,6 @@ import gooey_ui as gui from gooey_ui import state +import inspect META_TITLE = "Gooey Components" META_DESCRIPTION = "Explore the Gooey Component Library" @@ -20,75 +21,72 @@ def render(): render_components() +def get_source_code(fn: callable): + source_code = inspect.getsource(fn) + return source_code + + def render_layouts(): section_title("Layouts") + + # Full Width Layout sub_section_title("Full Width Layout") - with gui.tag("div", className="container-fluid bg-light p-4"): - with gui.tag("div", className="row"): - with gui.tag("div", className="col-12"): - gui.html("This is a full width layout") - code_block( - """ + + def full_width_layout(): with gui.tag("div", className="container-fluid bg-light p-4"): with gui.tag("div", className="row"): with gui.tag("div", className="col-12"): - gui.html("This is a full width layout")""" - ) + gui.html("This is a full width layout") + + full_width_layout() + code_block(get_source_code(full_width_layout)) + + # 1/2 Layout sub_section_title("Full Width 1/2 Layout") - with gui.tag("div", className="container-fluid p-2"): - with gui.tag("div", className="row"): - with gui.tag("div", className="col-6 border"): - gui.html("This is a 1/2 width layout") - with gui.tag("div", className="col-6 border"): - gui.html("This is a 1/2 width layout") - code_block( - """with gui.tag("div", className="container-fluid p-4"): - with gui.tag("div", className="row"): - with gui.tag("div", className="col-6 border"): - gui.html("This is a 1/2 width layout") - with gui.tag("div", className="col-6 border"): - gui.html("This is a 1/2 width layout")""" - ) + + def half_width_layout(): + with gui.tag("div", className="container-fluid p-2"): + with gui.tag("div", className="row"): + with gui.tag("div", className="col-6 border"): + gui.html("This is a 1/2 width layout") + with gui.tag("div", className="col-6 border"): + gui.html("This is a 1/2 width layout") + + half_width_layout() + code_block(get_source_code(half_width_layout)) + + # 1/3 Layout sub_section_title("Full Width 1/3 Layout") - with gui.tag("div", className="container-fluid p-2"): - with gui.tag("div", className="row"): - with gui.tag("div", className="col-4 border"): - gui.html("This is a 1/3 width layout") - with gui.tag("div", className="col-4 border"): - gui.html("This is a 1/3 width layout") - with gui.tag("div", className="col-4 border"): - gui.html("This is a 1/3 width layout") - code_block( - """with gui.tag("div", className="container-fluid p-2"): - with gui.tag("div", className="row"): - with gui.tag("div", className="col-4 border"): - gui.html("This is a 1/3 width layout") - with gui.tag("div", className="col-4 border"): - gui.html("This is a 1/3 width layout") - with gui.tag("div", className="col-4 border"): - gui.html("This is a 1/3 width layout")""" - ) + + def third_width_layout(): + with gui.tag("div", className="container-fluid p-2"): + with gui.tag("div", className="row"): + with gui.tag("div", className="col-4 border"): + gui.html("This is a 1/3 width layout") + with gui.tag("div", className="col-4 border"): + gui.html("This is a 1/3 width layout") + with gui.tag("div", className="col-4 border"): + gui.html("This is a 1/3 width layout") + + third_width_layout() + code_block(get_source_code(third_width_layout)) + + # Responsive 1/3 Layout sub_section_title("Responsive 1/3 Layout") gui.write("These columns will go full width on small devices") - with gui.tag("div", className="container-fluid p-2"): - with gui.tag("div", className="row"): - with gui.tag("div", className="col-12 col-md-4 border"): - gui.html("This is a responsive 1/3 width layout") - with gui.tag("div", className="col-12 col-md-4 border"): - gui.html("This is a responsive 1/3 width layout") - with gui.tag("div", className="col-12 col-md-4 border"): - gui.html("This is a responsive 1/3 width layout") - code_block( - """with gui.tag("div", className="container-fluid p-2"): - with gui.tag("div", className="row"): - with gui.tag("div", className="col-12 col-md-4 border"): - gui.html("This is a responsive 1/3 width layout") - with gui.tag("div", className="col-12 col-md-4 border"): - gui.html("This is a responsive 1/3 width layout") - with gui.tag("div", className="col-12 col-md-4 border"): - gui.html("This is a responsive 1/3 width layout") - """ - ) + + def responsive_third_width_layout(): + with gui.tag("div", className="container-fluid p-2"): + with gui.tag("div", className="row"): + with gui.tag("div", className="col-12 col-md-4 border"): + gui.html("This is a responsive 1/3 width layout") + with gui.tag("div", className="col-12 col-md-4 border"): + gui.html("This is a responsive 1/3 width layout") + with gui.tag("div", className="col-12 col-md-4 border"): + gui.html("This is a responsive 1/3 width layout") + + responsive_third_width_layout() + code_block(get_source_code(responsive_third_width_layout)) def render_content(): @@ -97,51 +95,68 @@ def render_content(): with gui.tag("div", className="row"): # LEFT SIDE with gui.tag("div", className="col-12 col-md-6"): + + def render_headings(): + sub_section_title("Headings") + + def render_h1_to_h6_headings(): + with gui.tag("h1"): + gui.html("This is a h1 heading") + with gui.tag("h2"): + gui.html("This is a h2 heading") + with gui.tag("h3"): + gui.html("This is a h3 heading") + with gui.tag("h4"): + gui.html("This is a h4 heading") + with gui.tag("h5"): + gui.html("This is a h5 heading") + with gui.tag("h6"): + gui.html("This is a h6 heading") + + render_h1_to_h6_headings() + code_block(get_source_code(render_h1_to_h6_headings)) + render_headings() # RIGHT SIDE with gui.tag("div", className="col-12 col-md-6"): sub_section_title("Normal Text") - gui.write("This is a normal text") - code_block('gui.write("This is a normal text")') + + def normal_text(): + gui.write("This is a normal text") + + normal_text() + code_block(get_source_code(normal_text)) + sub_section_title("Link") - with gui.tag("a", href="https://www.gooey.ai"): - gui.html("This is a link") - code_block( - """with gui.tag("a", href="https://www.gooey.ai"): - gui.html("This is a link")""" - ) + + def link(): + with gui.tag("a", href="https://www.gooey.ai"): + gui.html("This is a link") + + link() + code_block(get_source_code(link)) + sub_section_title("Colored Text") - with gui.tag("p", className="text-primary"): - gui.html("This is a primary text") - with gui.tag("p", className="text-secondary"): - gui.html("This is a secondary text") - with gui.tag("p", className="text-success"): - gui.html("This is a success text") - with gui.tag("p", className="text-danger"): - gui.html("This is a danger text") - with gui.tag("p", className="text-warning"): - gui.html("This is a warning text") - with gui.tag("p", className="text-info"): - gui.html("This is a info text") - with gui.tag("p", className="text-light bg-dark"): - gui.html("This is a light text") - code_block( - """with gui.tag("p", className="text-primary"): - gui.html("This is a primary text") - with gui.tag("p", className="text-secondary"): - gui.html("This is a secondary text") - with gui.tag("p", className="text-success"): - gui.html("This is a success text") - with gui.tag("p", className="text-danger"): - gui.html("This is a danger text") - with gui.tag("p", className="text-warning"): - gui.html("This is a warning text") - with gui.tag("p", className="text-info"): - gui.html("This is a info text") - with gui.tag("p", className="text-light bg-dark"): - gui.html("This is a light text")""" - ) + + def colored_text(): + with gui.tag("p", className="text-primary"): + gui.html("This is a primary text") + with gui.tag("p", className="text-secondary"): + gui.html("This is a secondary text") + with gui.tag("p", className="text-success"): + gui.html("This is a success text") + with gui.tag("p", className="text-danger"): + gui.html("This is a danger text") + with gui.tag("p", className="text-warning"): + gui.html("This is a warning text") + with gui.tag("p", className="text-info"): + gui.html("This is a info text") + with gui.tag("p", className="text-light bg-dark"): + gui.html("This is a light text") + + colored_text() + code_block(get_source_code(colored_text)) def render_components(): @@ -164,172 +179,130 @@ def render_components(): # Inputs render_inputs() section_title("File Upload Button") - file = gui.file_uploader( - "**Upload Any File**", - key="file_uploader_test0", - help="Attach a video/audio/file to this.", - optional=True, - accept=["audio/*"], - ) - - -def render_headings(): - sub_section_title("Headings") - with gui.tag("h1"): - gui.html("This is a h1 heading") - with gui.tag("h2"): - gui.html("This is a h2 heading") - with gui.tag("h3"): - gui.html("This is a h3 heading") - with gui.tag("h4"): - gui.html("This is a h4 heading") - with gui.tag("h5"): - gui.html("This is a h5 heading") - with gui.tag("h6"): - gui.html("This is a h6 heading") - - code_block( - """ - with gui.tag("h1"): - gui.html("This is a h1 heading") - with gui.tag("h2"): - gui.html("This is a h2 heading") - with gui.tag("h3"): - gui.html("This is a h3 heading") - with gui.tag("h4"): - gui.html("This is a h4 heading") - with gui.tag("h5"): - gui.html("This is a h5 heading") - with gui.tag("h6"): - gui.html("This is a h6 heading") - """ - ) + + def file_upload(): + file = gui.file_uploader( + "**Upload Any File**", + key="file_uploader_test0", + help="Attach a video/audio/file to this.", + optional=True, + accept=["audio/*"], + ) + + file_upload() + code_block(get_source_code(file_upload)) def render_inputs(): section_title("Inputs") sub_section_title("Text Area") - gui.text_area( - "Label: Take some input from user", - "", - 100, - "textArea_test0", - "want help?", - "You can also show a placeholder", - ) - code_block( - 'gui.text_area("Example Text Area","",100,"textAreat_test0","want help?","You can also show a placeholder")' - ) + + def text_area(): + gui.text_area( + "Label: Take some input from user", + "", + 100, + "textArea_test0", + "want help?", + "You can also show a placeholder", + ) + + text_area() + code_block(get_source_code(text_area)) sub_section_title("Multi Select") - gui.multiselect( - "Label: Click below to select multiple options", - ["Option 1", "Option 2", "Option 3", "Option 4"], - ) - code_block( - 'gui.multiselect("Multi Select", ["Option 1", "Option 2", "Option 3", "Option 4"])' - ) + + def multi_select(): + gui.multiselect( + "Label: Click below to select multiple options", + ["Option 1", "Option 2", "Option 3", "Option 4"], + ) + + multi_select() + code_block(get_source_code(multi_select)) def render_alerts(): section_title("Alerts") - gui.success("Yay, this is done!") - code_block('gui.success("Yay, this is done!")') - gui.error("Opps, please try again!") - code_block('gui.error("Opps, please try again!")') + def success_alert(): + gui.success("Yay, this is done!") + + success_alert() + code_block(get_source_code(success_alert)) + + def error_alert(): + gui.error("Oops, please try again!") + + error_alert() + code_block(get_source_code(error_alert)) def render_tabs_example(): section_title("Tabs") # Rounded Tabs sub_section_title("Rounded Tabs") - tab1, tab2, tab3 = gui.tabs(["Tab 1", "Tab 2", "Tab 3"]) - with tab1: - gui.write("This is tab 1 content") - with tab2: - gui.write("This is tab 2 content") - with tab3: - gui.write("This is tab 3 content") - - code_block( - """ - tab1, tab2, tab3 = gui.tabs(["Tab 1", "Tab 2", "Tab 3"]) - with tab1: - gui.html("This is tab 1 content") - with tab2: - gui.html("This is tab 2 content") - with tab3: - gui.html("This is tab 3 content") - """ - ) - # Underline Tabs - selected_tab = "Tab 1" - sub_section_title("Underline Tabs") - with gui.nav_tabs(): - for name in ["Tab 1", "Tab 2", "Tab 4"]: - url = "/components" - with gui.nav_item(url, active=name == selected_tab): - gui.html(name) - with gui.nav_tab_content(): - if selected_tab == "Tab 1": + def rounded_tabs(): + tab1, tab2, tab3 = gui.tabs(["Tab 1", "Tab 2", "Tab 3"]) + with tab1: gui.write("This is tab 1 content") - elif selected_tab == "Tab 2": + with tab2: gui.write("This is tab 2 content") - else: + with tab3: gui.write("This is tab 3 content") - code_block( - """ - with gui.nav_tabs(): - for name in ["Tab 1", "Tab 2", "Tab 4"]: - url = "/components" - with gui.nav_item(url, active=name == selected_tab): - gui.html(name) - with gui.nav_tab_content(): - if selected_tab == "Tab 1": - gui.write("This is tab 1 content") - elif selected_tab == "Tab 2": - gui.write("This is tab 2 content") - else: - gui.write("This is tab 3 content") - """ - ) + rounded_tabs() + code_block(get_source_code(rounded_tabs)) + + # Underline Tabs + selected_tab = "Tab 1" + sub_section_title("Underline Tabs") + + def underline_tabs(): + with gui.nav_tabs(): + for name in ["Tab 1", "Tab 2", "Tab 4"]: + url = "/components" + with gui.nav_item(url, active=name == selected_tab): + gui.html(name) + with gui.nav_tab_content(): + if selected_tab == "Tab 1": + gui.write("This is tab 1 content") + elif selected_tab == "Tab 2": + gui.write("This is tab 2 content") + else: + gui.write("This is tab 3 content") + + underline_tabs() + code_block(get_source_code(underline_tabs)) def buttons_group(): sub_section_title("Buttons") - with gui.tag("div"): - with gui.tag("div", className="d-flex justify-content-around"): - gui.button("Primary", key="test0", type="primary") - gui.button("Secondary", key="test1") - gui.button("Tertiary", key="test3", type="tertiary") - gui.button("Link Button", key="test3", type="link") - code_block('gui.button("Primary", key="test0", type="primary")') - code_block('gui.button("Secondary", key="test1")') - code_block('gui.button("Tertiary", key="test3", type="tertiary")') - code_block('gui.button("Link Button", key="test3", type="link")') + def buttons(): + with gui.tag("div"): + with gui.tag("div", className="d-flex justify-content-around"): + gui.button("Primary", key="test0", type="primary") + gui.button("Secondary", key="test1") + gui.button("Tertiary", key="test3", type="tertiary") + gui.button("Link Button", key="test3", type="link") + + buttons() + code_block(get_source_code(buttons)) def code_block(content: str): with gui.tag("div", className="mt-4"): gui.write( rf""" - ```python - %s + ```python %s """ % content, unsafe_allow_html=True, ) -def collapsible_section(title: str) -> state.NestingCtx: - with gui.expander(title): - return state.NestingCtx() - - def section_title(title: str): with gui.tag("div", className="mb-4 mt-4 bg-light border-1 p-2"): with gui.tag("h3", style={"font-weight": "500", "margin": "0"}): From 20c2ee5242357987394aa6de86aa86417a4d6174 Mon Sep 17 00:00:00 2001 From: anish-work Date: Thu, 4 Jul 2024 14:31:18 +0530 Subject: [PATCH 16/17] use decorator function to show code --- components_doc.py | 45 ++++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/components_doc.py b/components_doc.py index 569ae80af..ccb8b64ac 100644 --- a/components_doc.py +++ b/components_doc.py @@ -21,6 +21,15 @@ def render(): render_components() +def show_source_code(fn): + def wrapper(*args, **kwargs): + out = fn(*args, **kwargs) + code_block(inspect.getsource(fn)) + return out + + return wrapper + + def get_source_code(fn: callable): source_code = inspect.getsource(fn) return source_code @@ -32,6 +41,7 @@ def render_layouts(): # Full Width Layout sub_section_title("Full Width Layout") + @show_source_code def full_width_layout(): with gui.tag("div", className="container-fluid bg-light p-4"): with gui.tag("div", className="row"): @@ -39,11 +49,11 @@ def full_width_layout(): gui.html("This is a full width layout") full_width_layout() - code_block(get_source_code(full_width_layout)) # 1/2 Layout sub_section_title("Full Width 1/2 Layout") + @show_source_code def half_width_layout(): with gui.tag("div", className="container-fluid p-2"): with gui.tag("div", className="row"): @@ -53,11 +63,11 @@ def half_width_layout(): gui.html("This is a 1/2 width layout") half_width_layout() - code_block(get_source_code(half_width_layout)) # 1/3 Layout sub_section_title("Full Width 1/3 Layout") + @show_source_code def third_width_layout(): with gui.tag("div", className="container-fluid p-2"): with gui.tag("div", className="row"): @@ -69,12 +79,12 @@ def third_width_layout(): gui.html("This is a 1/3 width layout") third_width_layout() - code_block(get_source_code(third_width_layout)) # Responsive 1/3 Layout sub_section_title("Responsive 1/3 Layout") gui.write("These columns will go full width on small devices") + @show_source_code def responsive_third_width_layout(): with gui.tag("div", className="container-fluid p-2"): with gui.tag("div", className="row"): @@ -86,7 +96,6 @@ def responsive_third_width_layout(): gui.html("This is a responsive 1/3 width layout") responsive_third_width_layout() - code_block(get_source_code(responsive_third_width_layout)) def render_content(): @@ -99,6 +108,7 @@ def render_content(): def render_headings(): sub_section_title("Headings") + @show_source_code def render_h1_to_h6_headings(): with gui.tag("h1"): gui.html("This is a h1 heading") @@ -114,7 +124,6 @@ def render_h1_to_h6_headings(): gui.html("This is a h6 heading") render_h1_to_h6_headings() - code_block(get_source_code(render_h1_to_h6_headings)) render_headings() @@ -122,23 +131,24 @@ def render_h1_to_h6_headings(): with gui.tag("div", className="col-12 col-md-6"): sub_section_title("Normal Text") + @show_source_code def normal_text(): gui.write("This is a normal text") normal_text() - code_block(get_source_code(normal_text)) sub_section_title("Link") + @show_source_code def link(): with gui.tag("a", href="https://www.gooey.ai"): gui.html("This is a link") link() - code_block(get_source_code(link)) sub_section_title("Colored Text") + @show_source_code def colored_text(): with gui.tag("p", className="text-primary"): gui.html("This is a primary text") @@ -156,7 +166,6 @@ def colored_text(): gui.html("This is a light text") colored_text() - code_block(get_source_code(colored_text)) def render_components(): @@ -180,6 +189,7 @@ def render_components(): render_inputs() section_title("File Upload Button") + @show_source_code def file_upload(): file = gui.file_uploader( "**Upload Any File**", @@ -190,13 +200,13 @@ def file_upload(): ) file_upload() - code_block(get_source_code(file_upload)) def render_inputs(): section_title("Inputs") sub_section_title("Text Area") + @show_source_code def text_area(): gui.text_area( "Label: Take some input from user", @@ -208,10 +218,10 @@ def text_area(): ) text_area() - code_block(get_source_code(text_area)) sub_section_title("Multi Select") + @show_source_code def multi_select(): gui.multiselect( "Label: Click below to select multiple options", @@ -219,23 +229,22 @@ def multi_select(): ) multi_select() - code_block(get_source_code(multi_select)) def render_alerts(): section_title("Alerts") + @show_source_code def success_alert(): gui.success("Yay, this is done!") success_alert() - code_block(get_source_code(success_alert)) + @show_source_code def error_alert(): gui.error("Oops, please try again!") error_alert() - code_block(get_source_code(error_alert)) def render_tabs_example(): @@ -243,6 +252,7 @@ def render_tabs_example(): # Rounded Tabs sub_section_title("Rounded Tabs") + @show_source_code def rounded_tabs(): tab1, tab2, tab3 = gui.tabs(["Tab 1", "Tab 2", "Tab 3"]) with tab1: @@ -253,12 +263,12 @@ def rounded_tabs(): gui.write("This is tab 3 content") rounded_tabs() - code_block(get_source_code(rounded_tabs)) # Underline Tabs selected_tab = "Tab 1" sub_section_title("Underline Tabs") + @show_source_code def underline_tabs(): with gui.nav_tabs(): for name in ["Tab 1", "Tab 2", "Tab 4"]: @@ -274,12 +284,12 @@ def underline_tabs(): gui.write("This is tab 3 content") underline_tabs() - code_block(get_source_code(underline_tabs)) def buttons_group(): sub_section_title("Buttons") + @show_source_code def buttons(): with gui.tag("div"): with gui.tag("div", className="d-flex justify-content-around"): @@ -289,16 +299,17 @@ def buttons(): gui.button("Link Button", key="test3", type="link") buttons() - code_block(get_source_code(buttons)) def code_block(content: str): + code_lines = content.split("\n")[1:] + formatted_code = "\n".join(code_lines) with gui.tag("div", className="mt-4"): gui.write( rf""" ```python %s """ - % content, + % formatted_code, unsafe_allow_html=True, ) From 714a663df349cf71a7cf84f4f63f3b91e7741c8a Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Mon, 8 Jul 2024 21:04:57 +0530 Subject: [PATCH 17/17] use columns for showing layouts in components page, remove inline functions --- components_doc.py | 311 +++++++++++++++++++++++----------------------- routers/root.py | 4 - 2 files changed, 155 insertions(+), 160 deletions(-) diff --git a/components_doc.py b/components_doc.py index ccb8b64ac..74c9cdde2 100644 --- a/components_doc.py +++ b/components_doc.py @@ -1,6 +1,7 @@ -import gooey_ui as gui -from gooey_ui import state import inspect +from functools import wraps + +import gooey_ui as gui META_TITLE = "Gooey Components" META_DESCRIPTION = "Explore the Gooey Component Library" @@ -22,6 +23,7 @@ def render(): def show_source_code(fn): + @wraps(fn) def wrapper(*args, **kwargs): out = fn(*args, **kwargs) code_block(inspect.getsource(fn)) @@ -30,176 +32,172 @@ def wrapper(*args, **kwargs): return wrapper -def get_source_code(fn: callable): - source_code = inspect.getsource(fn) - return source_code - - def render_layouts(): section_title("Layouts") # Full Width Layout sub_section_title("Full Width Layout") - - @show_source_code - def full_width_layout(): - with gui.tag("div", className="container-fluid bg-light p-4"): - with gui.tag("div", className="row"): - with gui.tag("div", className="col-12"): - gui.html("This is a full width layout") - full_width_layout() # 1/2 Layout sub_section_title("Full Width 1/2 Layout") - - @show_source_code - def half_width_layout(): - with gui.tag("div", className="container-fluid p-2"): - with gui.tag("div", className="row"): - with gui.tag("div", className="col-6 border"): - gui.html("This is a 1/2 width layout") - with gui.tag("div", className="col-6 border"): - gui.html("This is a 1/2 width layout") - half_width_layout() # 1/3 Layout sub_section_title("Full Width 1/3 Layout") - - @show_source_code - def third_width_layout(): - with gui.tag("div", className="container-fluid p-2"): - with gui.tag("div", className="row"): - with gui.tag("div", className="col-4 border"): - gui.html("This is a 1/3 width layout") - with gui.tag("div", className="col-4 border"): - gui.html("This is a 1/3 width layout") - with gui.tag("div", className="col-4 border"): - gui.html("This is a 1/3 width layout") - third_width_layout() # Responsive 1/3 Layout sub_section_title("Responsive 1/3 Layout") gui.write("These columns will go full width on small devices") + responsive_third_width_layout() - @show_source_code - def responsive_third_width_layout(): - with gui.tag("div", className="container-fluid p-2"): - with gui.tag("div", className="row"): - with gui.tag("div", className="col-12 col-md-4 border"): - gui.html("This is a responsive 1/3 width layout") - with gui.tag("div", className="col-12 col-md-4 border"): - gui.html("This is a responsive 1/3 width layout") - with gui.tag("div", className="col-12 col-md-4 border"): - gui.html("This is a responsive 1/3 width layout") - responsive_third_width_layout() +@show_source_code +def full_width_layout(): + with gui.div(className="w-100 bg-light p-4"): + gui.html("This is a full width layout") + + +@show_source_code +def half_width_layout(): + col1, col2 = gui.columns(2, responsive=False) + with col1: + with gui.div(className="border"): + gui.html("This is a 1/2 width layout") + with col2: + with gui.div(className="border"): + gui.html("This is a 1/2 width layout") + + +@show_source_code +def third_width_layout(): + col1, col2, col3 = gui.columns(3, responsive=False) + with col1: + with gui.div(className="border"): + gui.html("This is a 1/3 width layout") + with col2: + with gui.div(className="border"): + gui.html("This is a 1/3 width layout") + with col3: + with gui.div(className="border"): + gui.html("This is a 1/3 width layout") + + +@show_source_code +def responsive_third_width_layout(): + col1, col2, col3 = gui.columns(3) + with col1: + with gui.div(className="border"): + gui.html("This is a responsive 1/3 width layout") + with col2: + with gui.div(className="border"): + gui.html("This is a responsive 1/3 width layout") + with col3: + with gui.div(className="border"): + gui.html("This is a responsive 1/3 width layout") def render_content(): section_title("Content") - with gui.tag("div", className="container-fluid"): - with gui.tag("div", className="row"): - # LEFT SIDE - with gui.tag("div", className="col-12 col-md-6"): - - def render_headings(): - sub_section_title("Headings") - - @show_source_code - def render_h1_to_h6_headings(): - with gui.tag("h1"): - gui.html("This is a h1 heading") - with gui.tag("h2"): - gui.html("This is a h2 heading") - with gui.tag("h3"): - gui.html("This is a h3 heading") - with gui.tag("h4"): - gui.html("This is a h4 heading") - with gui.tag("h5"): - gui.html("This is a h5 heading") - with gui.tag("h6"): - gui.html("This is a h6 heading") - - render_h1_to_h6_headings() - - render_headings() - - # RIGHT SIDE - with gui.tag("div", className="col-12 col-md-6"): - sub_section_title("Normal Text") - - @show_source_code - def normal_text(): - gui.write("This is a normal text") - - normal_text() - - sub_section_title("Link") - - @show_source_code - def link(): - with gui.tag("a", href="https://www.gooey.ai"): - gui.html("This is a link") - - link() - - sub_section_title("Colored Text") - - @show_source_code - def colored_text(): - with gui.tag("p", className="text-primary"): - gui.html("This is a primary text") - with gui.tag("p", className="text-secondary"): - gui.html("This is a secondary text") - with gui.tag("p", className="text-success"): - gui.html("This is a success text") - with gui.tag("p", className="text-danger"): - gui.html("This is a danger text") - with gui.tag("p", className="text-warning"): - gui.html("This is a warning text") - with gui.tag("p", className="text-info"): - gui.html("This is a info text") - with gui.tag("p", className="text-light bg-dark"): - gui.html("This is a light text") - - colored_text() + with gui.tag("div", className="container-fluid"), gui.tag("div", className="row"): + # LEFT SIDE + with gui.tag("div", className="col-12 col-md-6"): + render_headings() + # RIGHT SIDE + with gui.tag("div", className="col-12 col-md-6"): + sub_section_title("Normal Text") + normal_text() + + sub_section_title("Link") + link() + + sub_section_title("Colored Text") + colored_text() + + +def render_headings(): + sub_section_title("Headings") + render_h1_to_h6_headings() + + +@show_source_code +def render_h1_to_h6_headings(): + with gui.tag("h1"): + gui.html("This is a h1 heading") + with gui.tag("h2"): + gui.html("This is a h2 heading") + with gui.tag("h3"): + gui.html("This is a h3 heading") + with gui.tag("h4"): + gui.html("This is a h4 heading") + with gui.tag("h5"): + gui.html("This is a h5 heading") + with gui.tag("h6"): + gui.html("This is a h6 heading") + + +@show_source_code +def normal_text(): + gui.write("This is a normal text") + + +@show_source_code +def link(): + with gui.tag("a", href="https://www.gooey.ai"): + gui.html("This is a link") + + +@show_source_code +def colored_text(): + with gui.tag("p", className="text-primary"): + gui.html("This is a primary text") + with gui.tag("p", className="text-secondary"): + gui.html("This is a secondary text") + with gui.tag("p", className="text-success"): + gui.html("This is a success text") + with gui.tag("p", className="text-danger"): + gui.html("This is a danger text") + with gui.tag("p", className="text-warning"): + gui.html("This is a warning text") + with gui.tag("p", className="text-info"): + gui.html("This is a info text") + with gui.tag("p", className="text-light bg-dark"): + gui.html("This is a light text") def render_components(): section_title("Components") - with gui.tag("div", className="container-fluid"): - with gui.tag("div", className="row"): - # LEFT SIDE - with gui.tag("div", className="col-12 col-md-6"): - # BUTTONS - buttons_group() - - # TABS - render_tabs_example() - - # RIGHT SIDE - with gui.tag("div", className="col-12 col-md-6"): - # ALERTS - render_alerts() - - # Inputs - render_inputs() - section_title("File Upload Button") - - @show_source_code - def file_upload(): - file = gui.file_uploader( - "**Upload Any File**", - key="file_uploader_test0", - help="Attach a video/audio/file to this.", - optional=True, - accept=["audio/*"], - ) - - file_upload() + with gui.tag("div", className="container-fluid"), gui.tag("div", className="row"): + # LEFT SIDE + with gui.tag("div", className="col-12 col-md-6"): + # BUTTONS + buttons_group() + + # TABS + render_tabs_example() + + # RIGHT SIDE + with gui.tag("div", className="col-12 col-md-6"): + # ALERTS + render_alerts() + + # Inputs + render_inputs() + + section_title("File Upload Button") + file_upload() + + +@show_source_code +def file_upload(): + gui.file_uploader( + "**Upload Any File**", + key="file_uploader_test0", + help="Attach a video/audio/file to this.", + optional=True, + accept=["audio/*"], + ) def render_inputs(): @@ -288,17 +286,16 @@ def underline_tabs(): def buttons_group(): sub_section_title("Buttons") + buttons() - @show_source_code - def buttons(): - with gui.tag("div"): - with gui.tag("div", className="d-flex justify-content-around"): - gui.button("Primary", key="test0", type="primary") - gui.button("Secondary", key="test1") - gui.button("Tertiary", key="test3", type="tertiary") - gui.button("Link Button", key="test3", type="link") - buttons() +@show_source_code +def buttons(): + with gui.tag("div"), gui.tag("div", className="d-flex justify-content-around"): + gui.button("Primary", key="test0", type="primary") + gui.button("Secondary", key="test1") + gui.button("Tertiary", key="test3", type="tertiary") + gui.button("Link Button", key="test3", type="link") def code_block(content: str): @@ -315,9 +312,11 @@ def code_block(content: str): def section_title(title: str): - with gui.tag("div", className="mb-4 mt-4 bg-light border-1 p-2"): - with gui.tag("h3", style={"font-weight": "500", "margin": "0"}): - gui.html(title.upper()) + with ( + gui.tag("div", className="mb-4 mt-4 bg-light border-1 p-2"), + gui.tag("h3", style={"font-weight": "500", "margin": "0"}), + ): + gui.html(title.upper()) def sub_section_title(title: str): diff --git a/routers/root.py b/routers/root.py index ade410607..7242a4613 100644 --- a/routers/root.py +++ b/routers/root.py @@ -208,10 +208,6 @@ def file_upload(form_data: FormData = fastapi_request_form): return {"url": upload_file_from_bytes(filename, data, content_type)} -async def request_json(request: Request): - return await request.json() - - @app.post("/components/") @st.route def component_page(request: Request):