Skip to content

Commit 79c8d07

Browse files
athul-rsclaudepre-commit-ci[bot]
authored
UN-1722 [FEAT] Add export reminder for Prompt Studio projects in use (#1547)
* UN-1722 [FEAT] Add export reminder for Prompt Studio projects in use - Add backend API endpoint to check if project is used in deployments - Implement frontend change tracking for prompt modifications - Create yellow notification bar component with export action - Track changes when editing, adding, or deleting prompts - Clear notification after successful export - Check usage in API Deployments, ETL/Task Pipelines, and Manual Review This ensures users are reminded to export their Prompt Studio changes when the project is actively being used in deployments, preventing confusion about why changes don't take effect immediately. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix: Remove trailing commas to fix Prettier/ESLint build errors Removed 10 trailing commas from 4 files that were causing the Docker build to fail with Prettier violations: - DocumentParser.jsx: 3 locations (lines 86, 124, 179) - Header.jsx: 3 locations (lines 73, 176, 277) - ToolIde.jsx: 2 locations (lines 97, 223) - custom-tool-store.js: 2 locations (lines 83, 106) These changes ensure the code passes ESLint/Prettier checks during the build process without modifying any functionality. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * refactor: Fix CodeRabbit major issues - state carryover and useCallback pattern Fixed two major issues identified by CodeRabbit review: 1. Fixed state carryover bug in custom-tool-store.js - When switching tools, deploymentUsageInfo and lastExportedAt now properly reset to null instead of carrying over from previous tool - Prevents incorrect export reminders showing for wrong projects 2. Fixed useCallback pattern issue in ToolIde.jsx - Replaced isCheckingUsage state in useCallback deps with useRef - Prevents unnecessary callback recreations and potential race conditions - Simplified useEffect dependencies to only depend on the callback - Removed unused isCheckingUsage state variable These changes improve code quality and prevent potential bugs without affecting functionality. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * fix: Address all PR #1547 review comments from chandrasekharan-zipstack This commit addresses all actionable review comments from the code reviewer: ## Backend Changes (views.py, constants.py, exceptions.py) 1. **Import Location Fix** ✅ - Moved APIDeployment, Pipeline, and WorkflowEndpoint imports to top of file - Removed lazy imports from check_deployment_usage method - Follows Python best practices for import organization 2. **Deployment Type Enum** ✅ - Created DeploymentType class in constants.py with deployment type constants - Updated check_deployment_usage to use DeploymentType constants - Replaced hardcoded strings: "API Deployment", "ETL Pipeline", etc. - Improves maintainability and prevents typos 3. **Error Handling** ✅ - Created DeploymentUsageCheckError exception class - Changed check_deployment_usage to raise exception instead of returning error response - Provides better error handling and follows DRF exception patterns 4. **Function Naming** ✅ - Renamed _check_tool_usage to _check_tool_usage_in_workflows - More explicit function name clarifies it checks workflow usage specifically - Updated all calls in destroy() and check_deployment_usage() methods ## Frontend Changes (ToolIde.jsx, CustomToolsHelper.js) 5. **Store State Race Condition Fix** ✅ - Added explicit reset of hasUnsavedChanges, deploymentUsageInfo, lastExportedAt - Ensures fields don't carry over when switching between tools - Prevents incorrect export reminders showing for wrong projects 6. **Stale State Race Condition Fix** ✅ - Added check for current hasUnsavedChanges state after API response - Prevents showing export reminder if user exported during in-flight check - Uses customToolStore.getState() to get real-time state value ## Not Addressed (Requires Discussion) - Active filtering question: Needs product/architecture discussion - UX enhancement for clickable links: May be future enhancement All code quality and bug fix comments have been fully addressed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * fix: PR review comments * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix: PR review comments * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * coderabbit fixes commit * Fixes for export conditions --------- Co-authored-by: Claude <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 09f62be commit 79c8d07

File tree

11 files changed

+426
-22
lines changed

11 files changed

+426
-22
lines changed

backend/prompt_studio/prompt_studio_core_v2/constants.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,15 @@ class IndexingConstants:
180180
RUN_ID = "run_id"
181181

182182

183+
class DeploymentType:
184+
"""Deployment types where Prompt Studio tools can be used."""
185+
186+
API_DEPLOYMENT = "API Deployment"
187+
ETL_PIPELINE = "ETL Pipeline"
188+
TASK_PIPELINE = "Task Pipeline"
189+
HUMAN_QUALITY_REVIEW = "Human in the Loop"
190+
191+
183192
class DefaultValues:
184193
"""Default values used throughout the prompt studio helper."""
185194

backend/prompt_studio/prompt_studio_core_v2/exceptions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,8 @@ def __init__(self, detail: str | None = None, code: int | None = None):
111111
if code is not None:
112112
self.code = code
113113
super().__init__(detail, code)
114+
115+
116+
class DeploymentUsageCheckError(APIException):
117+
status_code = 500
118+
default_detail = "Failed to check deployment usage"

backend/prompt_studio/prompt_studio_core_v2/urls.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@
5555
{"get": "export_project", "post": "import_project"}
5656
)
5757

58+
prompt_studio_deployment_usage = PromptStudioCoreView.as_view(
59+
{"get": "check_deployment_usage"}
60+
)
61+
5862

5963
urlpatterns = format_suffix_patterns(
6064
[
@@ -134,5 +138,10 @@
134138
prompt_studio_project_transfer,
135139
name="prompt_studio_project_transfer_import",
136140
),
141+
path(
142+
"prompt-studio/<uuid:pk>/check_deployment_usage/",
143+
prompt_studio_deployment_usage,
144+
name="prompt_studio_deployment_usage",
145+
),
137146
]
138147
)

backend/prompt_studio/prompt_studio_core_v2/views.py

Lines changed: 138 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
from typing import Any
66

77
from account_v2.custom_exceptions import DuplicateData
8+
from api_v2.models import APIDeployment
89
from django.db import IntegrityError
910
from django.db.models import QuerySet
1011
from django.http import HttpRequest, HttpResponse
1112
from file_management.constants import FileInformationKey as FileKey
1213
from file_management.exceptions import FileNotFound
1314
from permissions.permission import IsOwner, IsOwnerOrSharedUserOrSharedToOrg
15+
from pipeline_v2.models import Pipeline
1416
from plugins import get_plugin
1517
from rest_framework import status, viewsets
1618
from rest_framework.decorators import action
@@ -21,6 +23,7 @@
2123
from utils.file_storage.helpers.prompt_studio_file_helper import PromptStudioFileHelper
2224
from utils.user_context import UserContext
2325
from utils.user_session import UserSessionUtils
26+
from workflow_manager.endpoint_v2.models import WorkflowEndpoint
2427

2528
from prompt_studio.prompt_profile_manager_v2.constants import (
2629
ProfileManagerErrors,
@@ -29,6 +32,7 @@
2932
from prompt_studio.prompt_profile_manager_v2.models import ProfileManager
3033
from prompt_studio.prompt_profile_manager_v2.serializers import ProfileManagerSerializer
3134
from prompt_studio.prompt_studio_core_v2.constants import (
35+
DeploymentType,
3236
FileViewTypes,
3337
ToolStudioErrors,
3438
ToolStudioPromptKeys,
@@ -37,6 +41,7 @@
3741
DocumentIndexingService,
3842
)
3943
from prompt_studio.prompt_studio_core_v2.exceptions import (
44+
DeploymentUsageCheckError,
4045
IndexingAPIError,
4146
MaxProfilesReachedError,
4247
ToolDeleteError,
@@ -119,27 +124,105 @@ def perform_destroy(self, instance: CustomTool) -> None:
119124
organization_id = UserSessionUtils.get_organization_id(self.request)
120125
instance.delete(organization_id)
121126

127+
def _check_tool_usage_in_workflows(self, instance: CustomTool) -> tuple[bool, set]:
128+
"""Check if a tool is being used in any workflows.
129+
130+
Args:
131+
instance: The CustomTool instance to check
132+
133+
Returns:
134+
Tuple of (is_used: bool, dependent_workflows: set)
135+
"""
136+
registry = getattr(instance, "prompt_studio_registries", None)
137+
if not registry:
138+
return False, set()
139+
140+
dependent_wfs = set(
141+
ToolInstance.objects.filter(tool_id=registry.pk)
142+
.values_list("workflow_id", flat=True)
143+
.distinct()
144+
)
145+
return bool(dependent_wfs), dependent_wfs
146+
147+
def _get_deployment_types(self, workflow_ids: set) -> set:
148+
"""Get all deployment types where the tool is used.
149+
150+
Args:
151+
workflow_ids: Set of workflow IDs to check
152+
153+
Returns:
154+
Set of deployment type strings
155+
"""
156+
deployment_types: set = set()
157+
158+
# Check API Deployments (include inactive to prevent drift)
159+
if APIDeployment.objects.filter(workflow_id__in=workflow_ids).exists():
160+
deployment_types.add(DeploymentType.API_DEPLOYMENT)
161+
162+
# Check Pipelines using mapping instead of if/elif
163+
pipeline_type_mapping = {
164+
Pipeline.PipelineType.ETL: DeploymentType.ETL_PIPELINE,
165+
Pipeline.PipelineType.TASK: DeploymentType.TASK_PIPELINE,
166+
}
167+
pipelines = (
168+
Pipeline.objects.filter(workflow_id__in=workflow_ids)
169+
.values_list("pipeline_type", flat=True)
170+
.distinct()
171+
)
172+
for pipeline_type in pipelines:
173+
if pipeline_type in pipeline_type_mapping:
174+
deployment_types.add(pipeline_type_mapping[pipeline_type])
175+
176+
# Check for Manual Review
177+
if WorkflowEndpoint.objects.filter(
178+
workflow_id__in=workflow_ids,
179+
connection_type=WorkflowEndpoint.ConnectionType.MANUALREVIEW,
180+
).exists():
181+
deployment_types.add(DeploymentType.HUMAN_QUALITY_REVIEW)
182+
183+
return deployment_types
184+
185+
def _format_deployment_types_message(self, deployment_types: set) -> str:
186+
"""Format deployment types into human-readable message.
187+
188+
Args:
189+
deployment_types: Set of deployment type strings
190+
191+
Returns:
192+
Formatted message string or empty string if no types
193+
"""
194+
if not deployment_types:
195+
return ""
196+
197+
types_list = sorted(deployment_types)
198+
if len(types_list) == 1:
199+
types_text = types_list[0]
200+
elif len(types_list) == 2:
201+
types_text = f"{types_list[0]} or {types_list[1]}"
202+
else:
203+
types_text = ", ".join(types_list[:-1]) + f", or {types_list[-1]}"
204+
205+
return (
206+
f"You have made changes to this Prompt Studio project. "
207+
f"This project is used in {types_text}. "
208+
f"Please export it for the changes to take effect in the deployment(s)."
209+
)
210+
122211
def destroy(
123212
self, request: Request, *args: tuple[Any], **kwargs: dict[str, Any]
124213
) -> Response:
125214
instance: CustomTool = self.get_object()
126215
# Checks if tool is exported
127-
if hasattr(instance, "prompt_studio_registry"):
128-
exported_tool_instances_in_use = ToolInstance.objects.filter(
129-
tool_id__exact=instance.prompt_studio_registry.pk
216+
is_used, dependent_wfs = self._check_tool_usage_in_workflows(instance)
217+
if is_used:
218+
logger.info(
219+
f"Cannot destroy custom tool {instance.tool_id},"
220+
f" depended by workflows {dependent_wfs}"
221+
)
222+
raise ToolDeleteError(
223+
"Failed to delete Prompt Studio project; it's used in other workflows."
224+
"Delete its usages first."
130225
)
131-
dependent_wfs = set()
132-
for tool_instance in exported_tool_instances_in_use:
133-
dependent_wfs.add(tool_instance.workflow_id)
134-
if len(dependent_wfs) > 0:
135-
logger.info(
136-
f"Cannot destroy custom tool {instance.tool_id},"
137-
f" depended by workflows {dependent_wfs}"
138-
)
139-
raise ToolDeleteError(
140-
"Failed to delete tool, its used in other workflows. "
141-
"Delete its usages first"
142-
)
143226
return super().destroy(request, *args, **kwargs)
144227

145228
def partial_update(
@@ -569,9 +652,9 @@ def export_tool(self, request: Request, pk: Any = None) -> Response:
569652
def export_tool_info(self, request: Request, pk: Any = None) -> Response:
570653
custom_tool = self.get_object()
571654
serialized_instances = None
572-
if hasattr(custom_tool, "prompt_studio_registry"):
655+
if hasattr(custom_tool, "prompt_studio_registries"):
573656
serialized_instances = PromptStudioRegistryInfoSerializer(
574-
custom_tool.prompt_studio_registry
657+
custom_tool.prompt_studio_registries
575658
).data
576659

577660
return Response(serialized_instances)
@@ -672,3 +755,41 @@ def import_project(self, request: Request) -> Response:
672755
{"error": "Failed to import project"},
673756
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
674757
)
758+
759+
@action(detail=True, methods=["get"])
760+
def check_deployment_usage(self, request: Request, pk: Any = None) -> Response:
761+
"""Check if the Prompt Studio project is used in any deployments.
762+
763+
This endpoint checks if the exported tool from this project is being used in:
764+
- API Deployments
765+
- ETL Pipelines
766+
- Task Pipelines
767+
- Manual Review (Human Quality Review)
768+
769+
Returns:
770+
Response: Contains is_used flag and deployment types where it's used
771+
"""
772+
try:
773+
instance: CustomTool = self.get_object()
774+
is_used, workflow_ids = self._check_tool_usage_in_workflows(instance)
775+
776+
deployment_info: dict = {
777+
"is_used": is_used,
778+
"deployment_types": [],
779+
"message": "",
780+
}
781+
782+
if is_used and workflow_ids:
783+
deployment_types = self._get_deployment_types(workflow_ids)
784+
deployment_info["deployment_types"] = list(deployment_types)
785+
deployment_info["message"] = self._format_deployment_types_message(
786+
deployment_types
787+
)
788+
789+
return Response(deployment_info, status=status.HTTP_200_OK)
790+
791+
except Exception as e:
792+
logger.error(f"Error checking deployment usage for tool {pk}: {e}")
793+
raise DeploymentUsageCheckError(
794+
detail=f"Failed to check deployment usage: {str(e)}"
795+
)

frontend/src/components/custom-tools/document-parser/DocumentParser.jsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ function DocumentParser({
116116
}
117117
}
118118

119+
// Mark that changes have been made when any prompt field is modified
120+
const { setHasUnsavedChanges } = useCustomToolStore.getState();
121+
setHasUnsavedChanges(true);
122+
119123
const index = promptsAndNotes.findIndex(
120124
(item) => item?.prompt_id === promptId
121125
);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
.export-reminder-bar {
2+
position: sticky;
3+
top: 0;
4+
z-index: 100;
5+
width: 100%;
6+
}
7+
8+
.export-reminder-bar .ant-alert {
9+
border-radius: 0;
10+
border: none;
11+
background-color: #fffbe6;
12+
border-bottom: 1px solid #ffe58f;
13+
}
14+
15+
.export-reminder-content {
16+
width: 100%;
17+
display: flex;
18+
align-items: center;
19+
justify-content: center;
20+
gap: 12px;
21+
}
22+
23+
.export-reminder-text {
24+
flex: 1;
25+
max-width: 800px;
26+
text-align: left;
27+
}
28+
29+
.export-reminder-button {
30+
margin-left: auto;
31+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { ExclamationCircleOutlined } from "@ant-design/icons";
2+
import { Alert, Button, Space } from "antd";
3+
import PropTypes from "prop-types";
4+
import "./ExportReminderBar.css";
5+
6+
function ExportReminderBar({ message, onExport, isExporting }) {
7+
return (
8+
<div className="export-reminder-bar">
9+
<Alert
10+
message={
11+
<Space className="export-reminder-content">
12+
<ExclamationCircleOutlined />
13+
<span className="export-reminder-text">{message}</span>
14+
<Button
15+
type="primary"
16+
size="small"
17+
onClick={onExport}
18+
loading={isExporting}
19+
className="export-reminder-button"
20+
>
21+
Export Now
22+
</Button>
23+
</Space>
24+
}
25+
type="warning"
26+
banner
27+
closable={false}
28+
/>
29+
</div>
30+
);
31+
}
32+
33+
ExportReminderBar.propTypes = {
34+
message: PropTypes.string.isRequired,
35+
onExport: PropTypes.func.isRequired,
36+
isExporting: PropTypes.bool,
37+
};
38+
39+
ExportReminderBar.defaultProps = {
40+
isExporting: false,
41+
};
42+
43+
export { ExportReminderBar };

frontend/src/components/custom-tools/header/Header.jsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ function Header({
4444
setOpenCloneModal,
4545
}) {
4646
const [isExportLoading, setIsExportLoading] = useState(false);
47-
const { details, isPublicSource } = useCustomToolStore();
47+
const { details, isPublicSource, markChangesAsExported } =
48+
useCustomToolStore();
4849
const { sessionDetails } = useSessionStore();
4950
const { setAlertDetails } = useAlertStore();
5051
const axiosPrivate = useAxiosPrivate();
@@ -92,6 +93,8 @@ function Header({
9293
type: "success",
9394
content: "Custom tool exported successfully",
9495
});
96+
// Clear the export reminder after successful export
97+
markChangesAsExported();
9598
})
9699
.catch((err) => {
97100
if (err?.response?.data?.errors[0]?.code === "warning") {
@@ -117,7 +120,7 @@ function Header({
117120

118121
handleExport(selectedUsers, toolDetail, isSharedWithEveryone, true);
119122
setConfirmModalVisible(false);
120-
}, [lastExportParams]);
123+
}, [lastExportParams, handleExport]);
121124

122125
const handleShare = (isEdit) => {
123126
try {

0 commit comments

Comments
 (0)