From 9403c7b2cc66e814c339c3a95bf6b827e07b19dd Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Mon, 1 Jun 2026 15:19:29 +0530 Subject: [PATCH 1/4] [FEAT] Auto-create default LLM profile in Prompt Studio + clearer upload tooltip Drive default profile creation from the creator's default-set adapters (UserDefaultAdapter) instead of frictionless-only adapters, so OSS/on-prem projects also get a profile once each of the 4 adapter types is configured. Skips silently when a usable default is missing for any type. Frontend: wrap the disabled upload button in a span so the antd tooltip fires, and branch the message to prompt adding an LLM profile when none exists vs setting a default when profiles exist. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../prompt_studio_helper.py | 86 +++++++++---------- .../manage-docs-modal/ManageDocsModal.css | 7 ++ .../manage-docs-modal/ManageDocsModal.jsx | 33 ++++--- 3 files changed, 65 insertions(+), 61 deletions(-) diff --git a/backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py b/backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py index 89b55f8b18..e512a84e45 100644 --- a/backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py +++ b/backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py @@ -8,11 +8,9 @@ from account_v2.constants import Common from account_v2.models import User -from adapter_processor_v2.constants import AdapterKeys -from adapter_processor_v2.models import AdapterInstance +from adapter_processor_v2.models import AdapterInstance, UserDefaultAdapter from django.conf import settings from django.db import transaction -from django.db.models.manager import BaseManager from plugins import get_plugin from rest_framework.exceptions import APIException from rest_framework.request import Request @@ -96,57 +94,51 @@ class PromptStudioHelper: def create_default_profile_manager(user: User, tool_id: uuid) -> None: """Create a default profile manager for a given user and tool. - Args: - user (User): The user for whom the default profile manager is - created. - tool_id (uuid): The ID of the tool for which the default profile - manager is created. - - Raises: - AdapterInstance.DoesNotExist: If no suitable adapter instance is - found for creating the default profile manager. + Builds the profile from the creator's default-set adapters + (UserDefaultAdapter). In cloud these are the frictionless adapters + seeded at onboarding; in OSS/on-prem they are auto-set as the user + adds the first adapter of each type. Skips silently when a usable + default is missing for any of the four types, so a project can still + be created before adapters are configured. Returns: None """ - try: - AdapterInstance.objects.get( - is_friction_less=True, - is_usable=True, - adapter_type=AdapterKeys.LLM, - ) - - default_adapters: BaseManager[AdapterInstance] = ( - AdapterInstance.objects.filter(is_friction_less=True) - ) + organization_member = OrganizationMemberService.get_user_by_id(id=user.id) + default_adapter = UserDefaultAdapter.objects.filter( + organization_member=organization_member + ).first() + if not default_adapter: + logger.info("Skipping default profile creation: no default adapters set") + return - profile_manager = ProfileManager( - prompt_studio_tool=CustomTool.objects.get(pk=tool_id), - is_default=True, - created_by=user, - modified_by=user, - chunk_size=0, - profile_name="sample profile", - chunk_overlap=0, - section="Default", - retrieval_strategy="simple", - similarity_top_k=3, + adapters = { + "llm": default_adapter.default_llm_adapter, + "embedding_model": default_adapter.default_embedding_adapter, + "vector_store": default_adapter.default_vector_db_adapter, + "x2text": default_adapter.default_x2text_adapter, + } + # A valid profile needs a usable default for every adapter type + if not all(adapter and adapter.is_usable for adapter in adapters.values()): + logger.info( + "Skipping default profile creation: " + "incomplete or unusable default adapters" ) + return - for adapter in default_adapters: - if adapter.adapter_type == AdapterKeys.LLM: - profile_manager.llm = adapter - elif adapter.adapter_type == AdapterKeys.VECTOR_DB: - profile_manager.vector_store = adapter - elif adapter.adapter_type == AdapterKeys.X2TEXT: - profile_manager.x2text = adapter - elif adapter.adapter_type == AdapterKeys.EMBEDDING: - profile_manager.embedding_model = adapter - - profile_manager.save() - - except AdapterInstance.DoesNotExist: - logger.info("skipping default profile creation") + ProfileManager.objects.create( + prompt_studio_tool=CustomTool.objects.get(pk=tool_id), + is_default=True, + created_by=user, + modified_by=user, + profile_name="sample profile", + chunk_size=0, + chunk_overlap=0, + section="Default", + retrieval_strategy="simple", + similarity_top_k=3, + **adapters, + ) @staticmethod def validate_adapter_status( diff --git a/frontend/src/components/custom-tools/manage-docs-modal/ManageDocsModal.css b/frontend/src/components/custom-tools/manage-docs-modal/ManageDocsModal.css index bb9c8e6932..036ffc552b 100644 --- a/frontend/src/components/custom-tools/manage-docs-modal/ManageDocsModal.css +++ b/frontend/src/components/custom-tools/manage-docs-modal/ManageDocsModal.css @@ -19,6 +19,13 @@ margin: 0; } +/* Span wrapper lets the tooltip fire over the disabled upload button, + which otherwise swallows hover events */ +.manage-docs-upload-tooltip-wrap { + display: inline-block; + width: 100%; +} + .summarization-status-circle { width: 8px; height: 8px; diff --git a/frontend/src/components/custom-tools/manage-docs-modal/ManageDocsModal.jsx b/frontend/src/components/custom-tools/manage-docs-modal/ManageDocsModal.jsx index 0c640ebaa2..a8abf9a208 100644 --- a/frontend/src/components/custom-tools/manage-docs-modal/ManageDocsModal.jsx +++ b/frontend/src/components/custom-tools/manage-docs-modal/ManageDocsModal.jsx @@ -766,22 +766,27 @@ function ManageDocsModal({ > - + + + From 949a8f50b82e1322712eb313dc82cd0db45cb985 Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Mon, 1 Jun 2026 16:10:50 +0530 Subject: [PATCH 2/4] [MISC] Name auto-created profile "Default Profile" via shared constant Reuse DefaultValues.DEFAULT_PROFILE_NAME instead of "sample profile" so the auto-created profile matches the name the import flow already defaults to. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py b/backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py index e512a84e45..ac28560f10 100644 --- a/backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py +++ b/backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py @@ -131,7 +131,7 @@ def create_default_profile_manager(user: User, tool_id: uuid) -> None: is_default=True, created_by=user, modified_by=user, - profile_name="sample profile", + profile_name=DefaultValues.DEFAULT_PROFILE_NAME, chunk_size=0, chunk_overlap=0, section="Default", From 34b941b6cd037718df1bd9893ea8b60f3055e686 Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Mon, 1 Jun 2026 18:57:19 +0530 Subject: [PATCH 3/4] [MISC] Make default profile creation best-effort so it never breaks project creation create_default_profile_manager is called outside the create() try/except, so wrap the ProfileManager insert and swallow/log any error - matching the prior contract where a missing adapter was silently skipped. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../prompt_studio_helper.py | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py b/backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py index ac28560f10..ace573b58d 100644 --- a/backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py +++ b/backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py @@ -126,19 +126,26 @@ def create_default_profile_manager(user: User, tool_id: uuid) -> None: ) return - ProfileManager.objects.create( - prompt_studio_tool=CustomTool.objects.get(pk=tool_id), - is_default=True, - created_by=user, - modified_by=user, - profile_name=DefaultValues.DEFAULT_PROFILE_NAME, - chunk_size=0, - chunk_overlap=0, - section="Default", - retrieval_strategy="simple", - similarity_top_k=3, - **adapters, - ) + # Best-effort: a profile creation hiccup must never break project creation + try: + ProfileManager.objects.create( + prompt_studio_tool=CustomTool.objects.get(pk=tool_id), + is_default=True, + created_by=user, + modified_by=user, + profile_name=DefaultValues.DEFAULT_PROFILE_NAME, + chunk_size=0, + chunk_overlap=0, + section="Default", + retrieval_strategy="simple", + similarity_top_k=3, + **adapters, + ) + except Exception: + logger.warning( + "Skipping default profile creation: failed to create profile", + exc_info=True, + ) @staticmethod def validate_adapter_status( From 88e1317331c87351e03944c79b1adf7c6059d28b Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Tue, 2 Jun 2026 10:15:44 +0530 Subject: [PATCH 4/4] [MISC] Address review: guard missing org member + savepoint default profile create - Return early when the user has no OrganizationMember, so the nullable UserDefaultAdapter.organization_member can't match an unrelated null-scoped row. - Wrap ProfileManager.create in transaction.atomic() so a caught DB error doesn't poison the request's outer transaction under ATOMIC_REQUESTS. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../prompt_studio_helper.py | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py b/backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py index ace573b58d..af6ce3a34b 100644 --- a/backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py +++ b/backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py @@ -105,6 +105,12 @@ def create_default_profile_manager(user: User, tool_id: uuid) -> None: None """ organization_member = OrganizationMemberService.get_user_by_id(id=user.id) + if not organization_member: + logger.info( + "Skipping default profile creation: user has no organization membership" + ) + return + default_adapter = UserDefaultAdapter.objects.filter( organization_member=organization_member ).first() @@ -126,21 +132,24 @@ def create_default_profile_manager(user: User, tool_id: uuid) -> None: ) return - # Best-effort: a profile creation hiccup must never break project creation + # Best-effort: a profile creation hiccup must never break project creation. + # The savepoint keeps a DB error from poisoning the request's outer + # transaction when ATOMIC_REQUESTS is enabled. try: - ProfileManager.objects.create( - prompt_studio_tool=CustomTool.objects.get(pk=tool_id), - is_default=True, - created_by=user, - modified_by=user, - profile_name=DefaultValues.DEFAULT_PROFILE_NAME, - chunk_size=0, - chunk_overlap=0, - section="Default", - retrieval_strategy="simple", - similarity_top_k=3, - **adapters, - ) + with transaction.atomic(): + ProfileManager.objects.create( + prompt_studio_tool=CustomTool.objects.get(pk=tool_id), + is_default=True, + created_by=user, + modified_by=user, + profile_name=DefaultValues.DEFAULT_PROFILE_NAME, + chunk_size=0, + chunk_overlap=0, + section="Default", + retrieval_strategy="simple", + similarity_top_k=3, + **adapters, + ) except Exception: logger.warning( "Skipping default profile creation: failed to create profile",