From 4e9b6b689f1070e6324da04ca7e44ddb53d1a6f4 Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Fri, 12 Jun 2026 18:35:40 -0700 Subject: [PATCH] feat: add surface field to SavedRun to track call site, deprecate is_api_call field, filter history and builder conversations by surface --- bots/admin.py | 6 +-- ...drun_surface_alter_savedrun_is_api_call.py | 54 +++++++++++++++++++ bots/models/published_run.py | 2 + bots/models/saved_run.py | 32 ++++++++++- bots/tasks.py | 2 + celeryapp/tasks.py | 2 +- daras_ai_v2/base.py | 10 ++-- daras_ai_v2/bot_integration_widgets.py | 2 + daras_ai_v2/bots.py | 1 + daras_ai_v2/breadcrumbs.py | 6 +-- daras_ai_v2/gooey_builder.py | 41 ++------------ daras_ai_v2/safety_checker.py | 2 + functions/gooey_builder_workflow_tools.py | 2 + functions/workflow_tools.py | 3 +- livekit_agent.py | 1 + recipes/BulkRunner.py | 2 + recipes/ModelTrainer.py | 1 + recipes/VideoBotsStats.py | 1 + routers/api.py | 12 +++-- routers/twilio_api.py | 3 +- widgets/demo_button.py | 1 + widgets/workflow_bulk_runs_list.py | 1 + widgets/workflow_image.py | 3 +- 23 files changed, 129 insertions(+), 61 deletions(-) create mode 100644 bots/migrations/0121_savedrun_surface_alter_savedrun_is_api_call.py diff --git a/bots/admin.py b/bots/admin.py index 4d8a434ee..da4ee0a72 100644 --- a/bots/admin.py +++ b/bots/admin.py @@ -449,7 +449,7 @@ class SavedRunAdmin(GooeyModelAdmin): "view_parent_published_run", "run_time", "price", - "is_api_call", + "surface", "is_cancelled", "created_at", "updated_at", @@ -459,7 +459,7 @@ class SavedRunAdmin(GooeyModelAdmin): ] list_filter = [ "workflow", - "is_api_call", + "surface", "is_cancelled", "is_flagged", ("run_status", admin.EmptyFieldListFilter), @@ -482,7 +482,7 @@ class SavedRunAdmin(GooeyModelAdmin): "created_at", "updated_at", "run_time", - "is_api_call", + "surface", "parent_builder_saved_run", "view_bot_message", ] diff --git a/bots/migrations/0121_savedrun_surface_alter_savedrun_is_api_call.py b/bots/migrations/0121_savedrun_surface_alter_savedrun_is_api_call.py new file mode 100644 index 000000000..d775268ec --- /dev/null +++ b/bots/migrations/0121_savedrun_surface_alter_savedrun_is_api_call.py @@ -0,0 +1,54 @@ +# Generated by Django 5.1.3 on 2026-06-13 04:31 + +from django.db import migrations, models + +from bots.models import SavedRun + + +def backfill_surface_from_is_api_call(apps, schema_editor): + SavedRunModel = apps.get_model("bots", "SavedRun") + db_alias = schema_editor.connection.alias + SavedRunModel.objects.using(db_alias).filter(is_api_call=True).update( + surface=SavedRun.Surface.api + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ("bots", "0120_remove_savedrun_bots_savedr_workflo_47a100_idx_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="savedrun", + name="surface", + field=models.IntegerField( + choices=[ + (0, "Run"), + (1, "API"), + (2, "Deployment"), + (3, "Builder Prompt"), + (4, "Builder Child"), + (5, "Tool Call"), + (6, "Internal"), + (7, "Analysis"), + (8, "Export"), + (9, "Bulk"), + ], + default=0, + help_text="Where this run was created.

Run: A run created from the UI playground directly by the user.
API: Called by an API.
Deployment: Called by a bot integration.
Builder Prompt: A prompt submitted to the Gooey builder.
Builder Child: A child run created by the Gooey builder.
Tool Call: Any tool calls made by a workflow.
Internal: Any internal calls made by the app, e.g. QR code or icon generator.
Analysis: A run created from the bot integration analysis feature.
Export: A run created from the scheduled bot integration export feature.
Bulk: A run created from the bulk runner.", + ), + ), + migrations.RunPython( + backfill_surface_from_is_api_call, + migrations.RunPython.noop, + ), + migrations.AlterField( + model_name="savedrun", + name="is_api_call", + field=models.BooleanField( + default=False, help_text="(Deprecated) Use surface instead." + ), + ), + ] diff --git a/bots/models/published_run.py b/bots/models/published_run.py index a4dc79906..9ff7a49a0 100644 --- a/bots/models/published_run.py +++ b/bots/models/published_run.py @@ -315,6 +315,7 @@ def submit_api_call( enable_rate_limits: bool = False, deduct_credits: bool = True, called_fn: CalledFunction | None = None, + **defaults, ) -> tuple[celery.result.AsyncResult, SavedRun]: return self.saved_run.submit_api_call( workspace=workspace, @@ -324,6 +325,7 @@ def submit_api_call( deduct_credits=deduct_credits, parent_pr=self, called_fn=called_fn, + **defaults, ) @classmethod diff --git a/bots/models/saved_run.py b/bots/models/saved_run.py index 6455352fa..cd37afe98 100644 --- a/bots/models/saved_run.py +++ b/bots/models/saved_run.py @@ -131,7 +131,37 @@ class SavedRun(models.Model): choices=RetentionPolicy.choices, default=RetentionPolicy.keep ) - is_api_call = models.BooleanField(default=False) + class Surface(IntegerChoices): + run = 0, "Run" + api = 1, "API" + deployment = 2, "Deployment" + builder_prompt = 3, "Builder Prompt" + builder_child = 4, "Builder Child" + tool_call = 5, "Tool Call" + internal = 6, "Internal" + analysis = 7, "Analysis" + export = 8, "Export" + bulk = 9, "Bulk" + + surface = models.IntegerField( + choices=Surface.choices, + default=Surface.run, + help_text="Where this run was created.

" + f"{Surface.run.label}: A run created from the UI playground directly by the user.
" + f"{Surface.api.label}: Called by an API.
" + f"{Surface.deployment.label}: Called by a bot integration.
" + f"{Surface.builder_prompt.label}: A prompt submitted to the Gooey builder.
" + f"{Surface.builder_child.label}: A child run created by the Gooey builder.
" + f"{Surface.tool_call.label}: Any tool calls made by a workflow.
" + f"{Surface.internal.label}: Any internal calls made by the app, e.g. QR code or icon generator.
" + f"{Surface.analysis.label}: A run created from the bot integration analysis feature.
" + f"{Surface.export.label}: A run created from the scheduled bot integration export feature.
" + f"{Surface.bulk.label}: A run created from the bulk runner.", + ) + is_api_call = models.BooleanField( + default=False, + help_text="(Deprecated) Use surface instead.", + ) # see signals.py:revoke_saved_run_task_on_cancel is_cancelled = models.BooleanField(default=False) diff --git a/bots/tasks.py b/bots/tasks.py index 9478c10f2..85ab15fad 100644 --- a/bots/tasks.py +++ b/bots/tasks.py @@ -16,6 +16,7 @@ Conversation, Message, Platform, + SavedRun, ) from daras_ai_v2.bots import save_msg_pair_to_db from daras_ai_v2.facebook_bots import WhatsappBot @@ -95,6 +96,7 @@ def msg_analysis(self, msg_id: int, anal_id: int, countdown: int | None): current_user=msg.conversation.bot_integration.created_by, request_body=dict(variables=variables), parent_pr=anal.published_run, + surface=SavedRun.Surface.analysis, ) # save the run before the result is ready diff --git a/celeryapp/tasks.py b/celeryapp/tasks.py index 248890578..7537bc317 100644 --- a/celeryapp/tasks.py +++ b/celeryapp/tasks.py @@ -180,7 +180,7 @@ def is_task_cancelled(e: BaseException | None) -> bool: def post_runner_tasks(saved_run_id: int): sr = SavedRun.objects.get(id=saved_run_id) - if not sr.is_api_call: + if sr.surface == SavedRun.Surface.run: send_email_on_completion(sr) if should_attempt_auto_recharge(sr.workspace): diff --git a/daras_ai_v2/base.py b/daras_ai_v2/base.py index 877dae5e8..6e63710dd 100644 --- a/daras_ai_v2/base.py +++ b/daras_ai_v2/base.py @@ -2350,8 +2350,6 @@ def _saved_tab(self): paginate_button(url=self.request.url, cursor=cursor) def _history_tab(self): - from daras_ai_v2.gooey_builder import get_default_builder_pr - self.ensure_authentication(anon_ok=True) # Filter by workspace @@ -2359,8 +2357,8 @@ def _history_tab(self): workflow=self.workflow, workspace=self.current_workspace ) - if settings.GOOEY_BUILDER_INTEGRATION_ID and not self.is_current_user_admin(): - qs = qs.exclude(parent_version__published_run=get_default_builder_pr()) + if not self.is_current_user_admin(): + qs = qs.exclude(surface=SavedRun.Surface.builder_prompt) # Apply user filter if specified for_param = self.request.query_params.get("for", "me") @@ -2369,7 +2367,9 @@ def _history_tab(self): and self.request.user and not self.current_workspace.is_personal ): - qs = qs.filter(uid=self.request.user.uid, is_api_call=False) + qs = qs.filter(uid=self.request.user.uid).exclude( + surface=SavedRun.Surface.deployment + ) run_history, cursor = paginate_queryset( qs=qs, ordering=["-updated_at"], cursor=self.request.query_params diff --git a/daras_ai_v2/bot_integration_widgets.py b/daras_ai_v2/bot_integration_widgets.py index 7e304c521..e06bad44b 100644 --- a/daras_ai_v2/bot_integration_widgets.py +++ b/daras_ai_v2/bot_integration_widgets.py @@ -13,6 +13,7 @@ BotIntegration, BotIntegrationAnalysisRun, Platform, + SavedRun, ) from daras_ai_v2 import icons, settings from daras_ai_v2.api_examples_widget import bot_api_example_generator @@ -576,6 +577,7 @@ def integration_details_generator(bi: BotIntegration, user: AppUser | None): workspace=bi.workspace, current_user=user, request_body=dict(variables=variables), + surface=SavedRun.Surface.internal, ) sr.wait_for_celery_result(result) # if failed, show error and abort diff --git a/daras_ai_v2/bots.py b/daras_ai_v2/bots.py index 74c91428a..bd55d173d 100644 --- a/daras_ai_v2/bots.py +++ b/daras_ai_v2/bots.py @@ -520,6 +520,7 @@ def _process_and_send_msg( workspace=workspace, current_user=current_user, request_body=body, + surface=SavedRun.Surface.deployment, ) bot.on_run_created(sr) diff --git a/daras_ai_v2/breadcrumbs.py b/daras_ai_v2/breadcrumbs.py index c6c705901..23ad9bc65 100644 --- a/daras_ai_v2/breadcrumbs.py +++ b/daras_ai_v2/breadcrumbs.py @@ -58,8 +58,6 @@ def get_title_breadcrumbs( is_root = pr and pr.saved_run == sr and pr.is_root() is_example = not is_root and pr and pr.saved_run == sr is_run = not is_root and not is_example - is_api_call = sr.is_api_call and tab == RecipeTabs.run - metadata = page_cls.workflow.get_or_create_metadata() root_title = TitleUrl( f"{metadata.emoji} {metadata.short_title}", page_cls.app_url() @@ -97,10 +95,8 @@ def get_title_breadcrumbs( case _ if is_run: if tab and tab.label: prefix = "Deployments" if tab == RecipeTabs.integrations else tab.label - elif is_api_call: - prefix = "API Run" else: - prefix = "Run" + prefix = sr.get_surface_display() prompt_title = page_cls.get_prompt_title(sr) if pr and not pr.is_root(): diff --git a/daras_ai_v2/gooey_builder.py b/daras_ai_v2/gooey_builder.py index 891bdd613..4521281ca 100644 --- a/daras_ai_v2/gooey_builder.py +++ b/daras_ai_v2/gooey_builder.py @@ -13,7 +13,6 @@ from bots.models import ( BotIntegration, SavedRun, - db_msgs_to_api_json, PublishedRun, ) from daras_ai_v2 import settings @@ -162,42 +161,6 @@ def can_launch_gooey_builder( # return current_workspace and current_workspace.enable_bot_builder -def get_conversation_data_for_saved_run( - bot_builder_integration: BotIntegration, sr: SavedRun -) -> dict | None: - """ - Returns conversation_data for the builder conversation associated with the given - SavedRun (messages up to gooey_builder_last_message_id), or None if not found. - """ - if not sr.parent_builder_saved_run: - return None - - from bots.models.convo_msg import Message - from routers.bots_api import api_hashids - - try: - last_message = Message.objects.get(saved_run=sr.parent_builder_saved_run) - except Message.DoesNotExist: - return None - - conversation = last_message.conversation - msgs_qs = conversation.messages.filter(created_at__lte=last_message.created_at) - messages = list( - db_msgs_to_api_json(msgs_qs.last_n_msgs(reset_at=conversation.reset_at)) - ) - data = dict( - bot_id=api_hashids.encode(bot_builder_integration.id), - user_id=conversation.web_user_id, - timestamp=conversation.created_at.isoformat(), - messages=messages, - ) - # only send the conversation id for a full conversation, - # for partial messages start a new conversation - if last_message == conversation.messages.latest(): - data["id"] = api_hashids.encode(conversation.id) - return data - - router = CustomAPIRouter() @@ -223,6 +186,7 @@ def gooey_builder_send_message(request: fastapi.Request, body: GooeyBuilderSendM parent_pr=workflow_pr, uid=request.user.uid, workspace_id=workspace.id, + surface=SavedRun.Surface.builder_child, ) workflow_sr.state.update(body.workflow_state) workflow_sr.save() @@ -239,6 +203,7 @@ def gooey_builder_send_message(request: fastapi.Request, body: GooeyBuilderSendM current_user=request.user, workspace=workspace, request_body=request_body, + surface=SavedRun.Surface.builder_prompt, )[1] workflow_sr.save(update_fields=["parent_builder_saved_run"]) @@ -272,7 +237,7 @@ def fetch_builder_conversations( workflow=Workflow.VIDEO_BOTS, workspace=get_current_workspace(request.user, request.session), uid=request.user.uid, - parent_builder_saved_run__isnull=False, + surface=SavedRun.Surface.builder_child, ) .annotate(title=F("parent_builder_saved_run__state__input_prompt")) .order_by("-updated_at")[: body.limit] diff --git a/daras_ai_v2/safety_checker.py b/daras_ai_v2/safety_checker.py index 812bff32d..682647517 100644 --- a/daras_ai_v2/safety_checker.py +++ b/daras_ai_v2/safety_checker.py @@ -1,6 +1,7 @@ from contextlib import contextmanager from app_users.models import AppUser +from bots.models import SavedRun from daras_ai_v2 import settings from daras_ai_v2.azure_image_moderation import is_image_nsfw from daras_ai_v2.exceptions import UserError @@ -35,6 +36,7 @@ def safety_checker_text(text_input: str): current_user=billing_account, request_body=dict(variables=dict(input=text_input)), deduct_credits=False, + surface=SavedRun.Surface.internal, ) ) diff --git a/functions/gooey_builder_workflow_tools.py b/functions/gooey_builder_workflow_tools.py index 98d2e3147..bda785a63 100644 --- a/functions/gooey_builder_workflow_tools.py +++ b/functions/gooey_builder_workflow_tools.py @@ -159,6 +159,7 @@ def call( parent_pr=pr, uid=self.builder_sr.uid, workspace_id=self.builder_sr.workspace_id, + surface=SavedRun.Surface.builder_child, ) # update the state @@ -221,6 +222,7 @@ def call(self, run_url: str | None = None, background: bool = False) -> typing.A enable_rate_limits=True, parent_builder_saved_run=self.builder_sr, parent_pr=pr, + surface=SavedRun.Surface.builder_child, ) else: page = page_cls(user=user) diff --git a/functions/workflow_tools.py b/functions/workflow_tools.py index b59dab49a..0d254458c 100644 --- a/functions/workflow_tools.py +++ b/functions/workflow_tools.py @@ -7,6 +7,7 @@ from django.utils.text import slugify from app_users.models import AppUser +from bots.models import SavedRun from daras_ai_v2.exceptions import UserError from functions.models import CalledFunction, FunctionTrigger from functions.base_llm_tool import ( @@ -17,7 +18,6 @@ if typing.TYPE_CHECKING: - from bots.models import SavedRun from daras_ai_v2.base import BasePage from workspaces.models import Workspace @@ -120,6 +120,7 @@ def call(self, **kwargs): request_body=request_body, deduct_credits=False, called_fn=called_fn, + surface=SavedRun.Surface.tool_call, ) self.fn_sr = fn_sr diff --git a/livekit_agent.py b/livekit_agent.py index 74ba18324..aa96f06ae 100644 --- a/livekit_agent.py +++ b/livekit_agent.py @@ -586,6 +586,7 @@ def create_run(bot: LivekitVoice): current_user=bot.bi.created_by, workspace=bot.bi.workspace, request_body=body, + surface=SavedRun.Surface.deployment, ) sr.transaction, sr.price = page.deduct_credits(sr.state) diff --git a/recipes/BulkRunner.py b/recipes/BulkRunner.py index b91b547f1..737b0f796 100644 --- a/recipes/BulkRunner.py +++ b/recipes/BulkRunner.py @@ -318,6 +318,7 @@ def run_v2( current_user=self.request.user, request_body=request_body, parent_pr=pr, + surface=SavedRun.Surface.bulk, ) sr.wait_for_celery_result(result) @@ -390,6 +391,7 @@ def run_v2( current_user=self.request.user, request_body=request_body, parent_pr=pr, + surface=SavedRun.Surface.bulk, ) sr.wait_for_celery_result(result) response.eval_runs.append(sr.get_app_url()) diff --git a/recipes/ModelTrainer.py b/recipes/ModelTrainer.py index 2f5f2af7a..1ca659cc5 100644 --- a/recipes/ModelTrainer.py +++ b/recipes/ModelTrainer.py @@ -282,6 +282,7 @@ def call_text2img_for_model( selected_models=[Text2ImgModels.flux_1_dev.name], quality=50, ).model_dump(exclude_unset=True), + surface=SavedRun.Surface.internal, )[1] return sr diff --git a/recipes/VideoBotsStats.py b/recipes/VideoBotsStats.py index 7ab267ecc..07d19ba05 100644 --- a/recipes/VideoBotsStats.py +++ b/recipes/VideoBotsStats.py @@ -718,6 +718,7 @@ def exec_export_fn(bi: BotIntegration, fn_sr: SavedRun, fn_pr: PublishedRun | No request_body=dict(variables=variables, variables_schema=variables_schema), parent_pr=fn_pr, current_user=bi.workspace.created_by, + surface=SavedRun.Surface.export, ) diff --git a/routers/api.py b/routers/api.py index a13fc715e..c1086cd27 100644 --- a/routers/api.py +++ b/routers/api.py @@ -28,7 +28,7 @@ from api_keys.models import ApiKey from app_users.models import AppUser from auth.token_authentication import api_auth_header -from bots.models import RetentionPolicy, Workflow +from bots.models import RetentionPolicy, SavedRun, Workflow 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 @@ -45,7 +45,6 @@ if typing.TYPE_CHECKING: from functions.models import CalledFunction - from bots.models import SavedRun import celery.result app = CustomAPIRouter() @@ -367,8 +366,9 @@ def submit_api_call( enable_rate_limits: bool = False, deduct_credits: bool = True, called_fn: typing.Optional["CalledFunction"] = None, + surface: SavedRun.Surface = SavedRun.Surface.api, **defaults, -) -> tuple["celery.result.AsyncResult", "SavedRun"]: +) -> tuple["celery.result.AsyncResult", SavedRun]: page, sr = create_new_run( page_cls=page_cls, query_params=query_params, @@ -377,6 +377,7 @@ def submit_api_call( workspace=workspace, request_body=request_body, enable_rate_limits=enable_rate_limits, + surface=surface, **defaults, ) if called_fn: @@ -396,8 +397,9 @@ def create_new_run( workspace: Workspace, request_body: dict, enable_rate_limits: bool = False, + surface: SavedRun.Surface = SavedRun.Surface.api, **defaults, -) -> tuple["BasePage", "SavedRun"]: +) -> tuple["BasePage", SavedRun]: # init a new page for every request query_params.setdefault("uid", current_user.uid) page = page_cls(user=current_user, query_params=query_params) @@ -422,8 +424,8 @@ def create_new_run( try: sr = page.create_new_run( enable_rate_limits=enable_rate_limits, - is_api_call=True, retention_policy=retention_policy or RetentionPolicy.keep, + surface=surface, **defaults, ) except ValidationError as e: diff --git a/routers/twilio_api.py b/routers/twilio_api.py index 2458ea6ac..5e8731f3f 100644 --- a/routers/twilio_api.py +++ b/routers/twilio_api.py @@ -11,7 +11,7 @@ from twilio.twiml.messaging_response import MessagingResponse from twilio.twiml.voice_response import VoiceResponse, Gather -from bots.models import Conversation, BotIntegration +from bots.models import Conversation, BotIntegration, SavedRun from daras_ai_v2 import settings from daras_ai_v2.asr import normalised_lang_in_collection from daras_ai_v2.bots import BotIntegrationLookupFailed, msg_handler @@ -281,6 +281,7 @@ def resp_say_or_tts_play( current_user=bot.current_user, workspace=bot.workspace, request_body=tts_state, + surface=SavedRun.Surface.deployment, ) # wait for the TTS to finish sr.wait_for_celery_result(result) diff --git a/widgets/demo_button.py b/widgets/demo_button.py index 865ec9409..0d43c7028 100644 --- a/widgets/demo_button.py +++ b/widgets/demo_button.py @@ -259,6 +259,7 @@ def run_qr_bot( response_format_type="json_object", ), deduct_credits=False, + surface=SavedRun.Surface.internal, ) gui.session_state[key + ":bot_run_id"] = sr.id return VideoBotsPage.realtime_channel_name(sr.run_id, sr.uid) diff --git a/widgets/workflow_bulk_runs_list.py b/widgets/workflow_bulk_runs_list.py index dbf7d453c..ff36618ea 100644 --- a/widgets/workflow_bulk_runs_list.py +++ b/widgets/workflow_bulk_runs_list.py @@ -151,6 +151,7 @@ def on_submit( workspace=workspace, request_body=dict(run_urls=[v1_url, v2_url]), current_user=user, + surface=SavedRun.Surface.bulk, ) url = new_sr.get_app_url() diff --git a/widgets/workflow_image.py b/widgets/workflow_image.py index f44dec076..3236cad35 100644 --- a/widgets/workflow_image.py +++ b/widgets/workflow_image.py @@ -3,7 +3,7 @@ import gooey_gui as gui from app_users.models import AppUser -from bots.models import PublishedRun +from bots.models import PublishedRun, SavedRun from daras_ai_v2 import icons, settings from widgets.workflow_metadata_gen import ( render_ai_generated_image_widget, @@ -80,6 +80,7 @@ def run_icon_bot( response_format_type="json_object", ), deduct_credits=False, + surface=SavedRun.Surface.internal, ) return VideoBotsPage.realtime_channel_name(sr.run_id, sr.uid)