diff --git a/backend/account_v2/authentication_service.py b/backend/account_v2/authentication_service.py index eaef0dde7e..591742821a 100644 --- a/backend/account_v2/authentication_service.py +++ b/backend/account_v2/authentication_service.py @@ -266,9 +266,6 @@ def get_invitations(self, organization_id: str) -> list[MemberInvitation]: def frictionless_onboarding(self, organization: Organization, user: User) -> None: raise MethodNotImplemented() - def hubspot_signup_api(self, request: Request) -> None: - raise MethodNotImplemented() - def delete_invitation(self, organization_id: str, invitation_id: str) -> bool: raise MethodNotImplemented() diff --git a/backend/api_v2/api_deployment_views.py b/backend/api_v2/api_deployment_views.py index a7aeeb7014..1b979f4278 100644 --- a/backend/api_v2/api_deployment_views.py +++ b/backend/api_v2/api_deployment_views.py @@ -220,6 +220,9 @@ def fetch_one(self, request: Request, pk: str | None = None) -> Response: def create( self, request: Request, *args: tuple[Any], **kwargs: dict[str, Any] ) -> Response: + # Check deployment count before create for HubSpot notification + deployment_count_before = APIDeployment.objects.count() + serializer: Serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) @@ -228,6 +231,9 @@ def create( {"api_key": api_key.api_key, **serializer.data} ) + # Notify HubSpot about API deployment + self._notify_hubspot_first_api_deploy(request.user, deployment_count_before) + headers = self.get_success_headers(serializer.data) return Response( response_serializer.data, @@ -235,6 +241,35 @@ def create( headers=headers, ) + def _notify_hubspot_first_api_deploy( + self, user, deployment_count_before: int + ) -> None: + """Notify HubSpot when an API is deployed. + + Checks if HubSpot plugin is available and notifies it about + the API deployment. The plugin decides whether to act based + on the is_first_for_org flag. + """ + hubspot_plugin = get_plugin("hubspot") + if not hubspot_plugin: + return + + try: + # First API deploy if count was 0 before deploy + is_first_for_org = deployment_count_before == 0 + + from plugins.integrations.hubspot import HubSpotEvent + + service = hubspot_plugin["service_class"]() + service.update_contact( + user=user, + events=[HubSpotEvent.API_DEPLOY], + is_first_for_org=is_first_for_org, + ) + except Exception as e: + # Log but don't fail the request + logger.warning(f"Failed to notify HubSpot for API deployment: {e}") + @action(detail=False, methods=["get"]) def by_prompt_studio_tool(self, request: Request) -> Response: """Get API deployments for a specific prompt studio tool.""" diff --git a/backend/prompt_studio/prompt_studio_core_v2/views.py b/backend/prompt_studio/prompt_studio_core_v2/views.py index 5e1f0d2a3f..8e610508ab 100644 --- a/backend/prompt_studio/prompt_studio_core_v2/views.py +++ b/backend/prompt_studio/prompt_studio_core_v2/views.py @@ -56,6 +56,8 @@ PromptStudioDocumentHelper, ) from prompt_studio.prompt_studio_index_manager_v2.models import IndexManager +from prompt_studio.prompt_studio_output_manager_v2.models import PromptStudioOutputManager +from prompt_studio.prompt_studio_registry_v2.models import PromptStudioRegistry from prompt_studio.prompt_studio_registry_v2.prompt_studio_registry_helper import ( PromptStudioRegistryHelper, ) @@ -118,8 +120,41 @@ def create(self, request: HttpRequest) -> Response: PromptStudioHelper.create_default_profile_manager( request.user, serializer.data["tool_id"] ) + + # Notify HubSpot if this is the first Prompt Studio project for the org + self._notify_hubspot_first_project(request.user) + return Response(serializer.data, status=status.HTTP_201_CREATED) + def _notify_hubspot_first_project(self, user) -> None: + """Notify HubSpot when a Prompt Studio project is created. + + Checks if HubSpot plugin is available and notifies it about + the project creation. The plugin decides whether to act based + on the is_first_for_org flag. + """ + hubspot_plugin = get_plugin("hubspot") + if not hubspot_plugin: + return + + try: + # Check if this is the first CustomTool for the organization + # (count == 1 means the one we just created is the first) + org_project_count = CustomTool.objects.count() + is_first_for_org = org_project_count == 1 + + from plugins.integrations.hubspot import HubSpotEvent + + service = hubspot_plugin["service_class"]() + service.update_contact( + user=user, + events=[HubSpotEvent.PROMPT_STUDIO_PROJECT_CREATE], + is_first_for_org=is_first_for_org, + ) + except Exception as e: + # Log but don't fail the request + logger.warning(f"Failed to notify HubSpot for project creation: {e}") + def perform_destroy(self, instance: CustomTool) -> None: organization_id = UserSessionUtils.get_organization_id(self.request) instance.delete(organization_id) @@ -408,6 +443,10 @@ def fetch_response(self, request: HttpRequest, pk: Any = None) -> Response: if not run_id: # Generate a run_id run_id = CommonUtils.generate_uuid() + + # Check output count before prompt run for HubSpot notification + output_count_before = PromptStudioOutputManager.objects.count() + response: dict[str, Any] = PromptStudioHelper.prompt_responder( id=id, tool_id=tool_id, @@ -417,8 +456,39 @@ def fetch_response(self, request: HttpRequest, pk: Any = None) -> Response: run_id=run_id, profile_manager_id=profile_manager, ) + + # Notify HubSpot about prompt run + self._notify_hubspot_first_prompt_run(request.user, output_count_before) + return Response(response, status=status.HTTP_200_OK) + def _notify_hubspot_first_prompt_run(self, user, output_count_before: int) -> None: + """Notify HubSpot when a prompt is run. + + Checks if HubSpot plugin is available and notifies it about + the prompt run. The plugin decides whether to act based + on the is_first_for_org flag. + """ + hubspot_plugin = get_plugin("hubspot") + if not hubspot_plugin: + return + + try: + # First prompt run if count was 0 before run + is_first_for_org = output_count_before == 0 + + from plugins.integrations.hubspot import HubSpotEvent + + service = hubspot_plugin["service_class"]() + service.update_contact( + user=user, + events=[HubSpotEvent.PROMPT_RUN], + is_first_for_org=is_first_for_org, + ) + except Exception as e: + # Log but don't fail the request + logger.warning(f"Failed to notify HubSpot for prompt run: {e}") + @action(detail=True, methods=["post"]) def single_pass_extraction(self, request: HttpRequest, pk: uuid) -> Response: """API Entry point method to fetch response to prompt. @@ -551,6 +621,9 @@ def upload_for_ide(self, request: HttpRequest, pk: Any = None) -> Response: uploaded_files: Any = serializer.validated_data.get("file") file_converter_plugin = get_plugin("file_converter") + # Check document count before upload for HubSpot notification + doc_count_before = DocumentManager.objects.count() + documents = [] for uploaded_file in uploaded_files: # Store file @@ -585,8 +658,39 @@ def upload_for_ide(self, request: HttpRequest, pk: Any = None) -> Response: "tool": document.tool.tool_id, } documents.append(doc) + + # Notify HubSpot about document upload + self._notify_hubspot_first_document(request.user, doc_count_before) + return Response({"data": documents}) + def _notify_hubspot_first_document(self, user, doc_count_before: int) -> None: + """Notify HubSpot when a document is uploaded. + + Checks if HubSpot plugin is available and notifies it about + the document upload. The plugin decides whether to act based + on the is_first_for_org flag. + """ + hubspot_plugin = get_plugin("hubspot") + if not hubspot_plugin: + return + + try: + # First document upload if count was 0 before upload + is_first_for_org = doc_count_before == 0 + + from plugins.integrations.hubspot import HubSpotEvent + + service = hubspot_plugin["service_class"]() + service.update_contact( + user=user, + events=[HubSpotEvent.DOCUMENT_UPLOAD], + is_first_for_org=is_first_for_org, + ) + except Exception as e: + # Log but don't fail the request + logger.warning(f"Failed to notify HubSpot for document upload: {e}") + @action(detail=True, methods=["delete"]) def delete_for_ide(self, request: HttpRequest, pk: uuid) -> Response: custom_tool = self.get_object() @@ -636,6 +740,9 @@ def export_tool(self, request: Request, pk: Any = None) -> Response: user_ids = set(serializer.validated_data.get("user_id")) force_export = serializer.validated_data.get("force_export") + # Check registry count before export for HubSpot notification + registry_count_before = PromptStudioRegistry.objects.count() + PromptStudioRegistryHelper.update_or_create_psr_tool( custom_tool=custom_tool, shared_with_org=is_shared_with_org, @@ -643,11 +750,41 @@ def export_tool(self, request: Request, pk: Any = None) -> Response: force_export=force_export, ) + # Notify HubSpot about tool export + self._notify_hubspot_first_tool_export(request.user, registry_count_before) + return Response( {"message": "Custom tool exported sucessfully."}, status=status.HTTP_200_OK, ) + def _notify_hubspot_first_tool_export(self, user, registry_count_before: int) -> None: + """Notify HubSpot when a tool is exported. + + Checks if HubSpot plugin is available and notifies it about + the tool export. The plugin decides whether to act based + on the is_first_for_org flag. + """ + hubspot_plugin = get_plugin("hubspot") + if not hubspot_plugin: + return + + try: + # First tool export if count was 0 before export + is_first_for_org = registry_count_before == 0 + + from plugins.integrations.hubspot import HubSpotEvent + + service = hubspot_plugin["service_class"]() + service.update_contact( + user=user, + events=[HubSpotEvent.TOOL_EXPORT], + is_first_for_org=is_first_for_org, + ) + except Exception as e: + # Log but don't fail the request + logger.warning(f"Failed to notify HubSpot for tool export: {e}") + @action(detail=True, methods=["get"]) def export_tool_info(self, request: Request, pk: Any = None) -> Response: custom_tool = self.get_object()