From 2e5f87299030bfed473d6c54332017b3d62f3f9c Mon Sep 17 00:00:00 2001 From: Rahul Johny <116638720+johnyrahul@users.noreply.github.com> Date: Thu, 20 Jun 2024 11:33:22 +0530 Subject: [PATCH 1/5] Performance degradtion fix (#411) * Restructered indexing API with non atomic requests * Code clean up * Manually correcting the migrations * Manually correcting the migrations * Updated the migration with comments --- .../prompt_studio_helper.py | 2 +- .../prompt_studio/prompt_studio_core/urls.py | 6 +- .../prompt_studio/prompt_studio_core/views.py | 2 +- .../prompt_studio_index_helper.py | 77 ++++++++++--------- .../tenant_account/migrations/0001_initial.py | 5 +- .../0002_organizationmember_delete_user.py | 66 ++++++++-------- 6 files changed, 84 insertions(+), 74 deletions(-) diff --git a/backend/prompt_studio/prompt_studio_core/prompt_studio_helper.py b/backend/prompt_studio/prompt_studio_core/prompt_studio_helper.py index 00d8d4309..ae6dbfde6 100644 --- a/backend/prompt_studio/prompt_studio_core/prompt_studio_helper.py +++ b/backend/prompt_studio/prompt_studio_core/prompt_studio_helper.py @@ -302,7 +302,7 @@ def index_document( """ tool: CustomTool = CustomTool.objects.get(pk=tool_id) if is_summary: - profile_manager = ProfileManager.objects.get( + profile_manager: ProfileManager = ProfileManager.objects.get( prompt_studio_tool=tool, is_summarize_llm=True ) default_profile = profile_manager diff --git a/backend/prompt_studio/prompt_studio_core/urls.py b/backend/prompt_studio/prompt_studio_core/urls.py index 3a32f63c4..0abc1c436 100644 --- a/backend/prompt_studio/prompt_studio_core/urls.py +++ b/backend/prompt_studio/prompt_studio_core/urls.py @@ -1,4 +1,6 @@ +from django.db import transaction from django.urls import path +from django.utils.decorators import method_decorator from rest_framework.urlpatterns import format_suffix_patterns from .views import PromptStudioCoreView @@ -77,7 +79,9 @@ ), path( "prompt-studio/index-document/", - prompt_studio_prompt_index, + method_decorator(transaction.non_atomic_requests)( + prompt_studio_prompt_index + ), name="prompt-studio-prompt-index", ), path( diff --git a/backend/prompt_studio/prompt_studio_core/views.py b/backend/prompt_studio/prompt_studio_core/views.py index 235e266b4..9093efd42 100644 --- a/backend/prompt_studio/prompt_studio_core/views.py +++ b/backend/prompt_studio/prompt_studio_core/views.py @@ -189,7 +189,7 @@ def make_profile_default(self, request: HttpRequest, pk: Any = None) -> Response data={"default_profile": profile_manager.profile_id}, ) - @action(detail=True, methods=["get"]) + @action(detail=True, methods=["post"]) def index_document(self, request: HttpRequest, pk: Any = None) -> Response: """API Entry point method to index input file. diff --git a/backend/prompt_studio/prompt_studio_index_manager/prompt_studio_index_helper.py b/backend/prompt_studio/prompt_studio_index_manager/prompt_studio_index_helper.py index 69fd5ba5b..49cae9141 100644 --- a/backend/prompt_studio/prompt_studio_index_manager/prompt_studio_index_helper.py +++ b/backend/prompt_studio/prompt_studio_index_manager/prompt_studio_index_helper.py @@ -1,6 +1,7 @@ import json import logging +from django.db import transaction from prompt_studio.prompt_profile_manager.models import ProfileManager from prompt_studio.prompt_studio_core.exceptions import IndexingAPIError from prompt_studio.prompt_studio_document_manager.models import DocumentManager @@ -18,46 +19,48 @@ def handle_index_manager( profile_manager: ProfileManager, doc_id: str, ) -> IndexManager: - document: DocumentManager = DocumentManager.objects.get(pk=document_id) + try: - index_id = "raw_index_id" - if is_summary: - index_id = "summarize_index_id" + with transaction.atomic(): - args: dict[str, str] = dict() - args["document_manager"] = document - args["profile_manager"] = profile_manager + document: DocumentManager = DocumentManager.objects.get(pk=document_id) - try: - # Create or get the existing record for this document and - # profile combo - index_manager, success = IndexManager.objects.get_or_create(**args) - - if success: - logger.info( - f"Index manager doc_id: {doc_id} for " - f"profile {profile_manager.profile_id} created" - ) - else: - logger.info( - f"Index manager doc_id: {doc_id} for " - f"profile {profile_manager.profile_id} updated" - ) - - index_ids = index_manager.index_ids_history - index_ids_list = json.loads(index_ids) if index_ids else [] - if doc_id not in index_ids: - index_ids_list.append(doc_id) - - args[index_id] = doc_id - args["index_ids_history"] = json.dumps(index_ids_list) - - # Update the record with the index id - result: IndexManager = IndexManager.objects.filter( - index_manager_id=index_manager.index_manager_id - ).update(**args) + index_id = "raw_index_id" + if is_summary: + index_id = "summarize_index_id" + + args: dict[str, str] = dict() + args["document_manager"] = document + args["profile_manager"] = profile_manager + + # Create or get the existing record for this document and + # profile combo + index_manager, success = IndexManager.objects.get_or_create(**args) + if success: + logger.info( + f"Index manager doc_id: {doc_id} for " + f"profile {profile_manager.profile_id} created" + ) + else: + logger.info( + f"Index manager doc_id: {doc_id} for " + f"profile {profile_manager.profile_id} updated" + ) + + index_ids = index_manager.index_ids_history + index_ids_list = json.loads(index_ids) if index_ids else [] + if doc_id not in index_ids: + index_ids_list.append(doc_id) + + args[index_id] = doc_id + args["index_ids_history"] = json.dumps(index_ids_list) + + # Update the record with the index id + result: IndexManager = IndexManager.objects.filter( + index_manager_id=index_manager.index_manager_id + ).update(**args) + return result except Exception as e: + transaction.rollback() raise IndexingAPIError("Error updating indexing status") from e - - return result diff --git a/backend/tenant_account/migrations/0001_initial.py b/backend/tenant_account/migrations/0001_initial.py index cad92899c..9cf3e1e69 100644 --- a/backend/tenant_account/migrations/0001_initial.py +++ b/backend/tenant_account/migrations/0001_initial.py @@ -14,8 +14,9 @@ class Migration(migrations.Migration): ] operations = [ + # Updated the name here as the 002, 0002 step is just name change migrations.CreateModel( - name="User", + name="OrganizationMember", fields=[ ( "user_ptr", @@ -28,6 +29,8 @@ class Migration(migrations.Migration): to=settings.AUTH_USER_MODEL, ), ), + # Added column which is used in 0002 here + ("role", models.CharField(default="admin")), ], options={ "verbose_name": "user", diff --git a/backend/tenant_account/migrations/0002_organizationmember_delete_user.py b/backend/tenant_account/migrations/0002_organizationmember_delete_user.py index 6a41c4aa8..68b4a63a2 100644 --- a/backend/tenant_account/migrations/0002_organizationmember_delete_user.py +++ b/backend/tenant_account/migrations/0002_organizationmember_delete_user.py @@ -1,9 +1,6 @@ # Generated by Django 4.2.1 on 2023-08-21 11:12 -import django.contrib.auth.models -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models +from django.db import migrations class Migration(migrations.Migration): @@ -13,33 +10,36 @@ class Migration(migrations.Migration): ] operations = [ - migrations.CreateModel( - name="OrganizationMember", - fields=[ - ( - "user_ptr", - models.OneToOneField( - auto_created=True, - on_delete=django.db.models.deletion.CASCADE, - parent_link=True, - primary_key=True, - serialize=False, - to=settings.AUTH_USER_MODEL, - ), - ), - ("role", models.CharField(default="admin")), - ], - options={ - "verbose_name": "user", - "verbose_name_plural": "users", - "abstract": False, - }, - bases=("account.user",), - managers=[ - ("objects", django.contrib.auth.models.UserManager()), - ], - ), - migrations.DeleteModel( - name="User", - ), + # # Commenting out here as this is taken care in 0001 + # migrations.CreateModel( + # name="OrganizationMember", + # fields=[ + # ( + # "user_ptr", + # models.OneToOneField( + # auto_created=True, + # on_delete=django.db.models.deletion.CASCADE, + # parent_link=True, + # primary_key=True, + # serialize=False, + # to=settings.AUTH_USER_MODEL, + # ), + # ), + # ("role", models.CharField(default="admin")), + # ], + # options={ + # "verbose_name": "user", + # "verbose_name_plural": "users", + # "abstract": False, + # }, + # bases=("account.user",), + # managers=[ + # ("objects", django.contrib.auth.models.UserManager()), + # ], + # ), + # # https://www.geeksforgeeks.org/what-is-access-exclusive-lock-mode-in-postgreysql/ + # # commenting drop table to ignore AccesExclusive Lock + # migrations.DeleteModel( + # name="User", + # ), ] From b3afbccf95358a51036644bbf3730ee63341525b Mon Sep 17 00:00:00 2001 From: Athul <89829560+athul-rs@users.noreply.github.com> Date: Thu, 20 Jun 2024 12:06:04 +0530 Subject: [PATCH 2/5] feat/app-deployment-plugin (#390) * feat/app-deployment-plugin * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * feature-flag enhancements * flags list api * fix/Enhancement changes * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix/pre-commit hook fix * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * sonar fix * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix/sonar * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * REST drf changes * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * router updated * flags review comment improvement * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * pre-commit hooks fix * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Empty-Commit --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Rahul Johny <116638720+johnyrahul@users.noreply.github.com> --- backend/backend/urls.py | 1 + backend/feature_flag/urls.py | 19 +- backend/feature_flag/views.py | 67 +- backend/utils/request/feature_flag.py | 17 +- frontend/src/assets/index.js | 2 + frontend/src/assets/placeholder.svg | 20 + .../navigations/side-nav-bar/SideNavBar.jsx | 23 +- .../app-deploy/AppDeploy.jsx | 92 - .../header/Header.css | 22 - .../header/Header.jsx | 54 - .../PipelinesOrDeployments.css | 56 - .../PipelinesOrDeployments.jsx | 264 --- frontend/src/helpers/FeatureFlagsData.js | 32 + frontend/src/helpers/GetSessionData.js | 1 + frontend/src/hooks/useSessionValid.js | 6 +- .../src/pages/PipelinesOrDeploymentsPage.jsx | 12 - frontend/src/routes/Router.jsx | 22 +- unstract/flags/src/unstract/flags/client.py | 87 - .../flags/src/unstract/flags/client/base.py | 17 + .../src/unstract/flags/client/evaluation.py | 52 + .../flags/src/unstract/flags/client/flipt.py | 55 + .../flags/{ => generated}/evaluation_pb2.py | 127 +- .../{ => generated}/evaluation_pb2_grpc.py | 1 + .../src/unstract/flags/generated/flipt_pb2.py | 198 ++ .../flags/generated/flipt_pb2_grpc.py | 1748 +++++++++++++++++ .../flags/{protos => proto}/evaluation.proto | 0 .../src/unstract/flags/proto/flipt.proto | 535 +++++ 27 files changed, 2809 insertions(+), 721 deletions(-) create mode 100644 frontend/src/assets/placeholder.svg delete mode 100644 frontend/src/components/pipelines-or-deployments/app-deploy/AppDeploy.jsx delete mode 100644 frontend/src/components/pipelines-or-deployments/header/Header.css delete mode 100644 frontend/src/components/pipelines-or-deployments/header/Header.jsx delete mode 100644 frontend/src/components/pipelines-or-deployments/pipelines-or-deployments/PipelinesOrDeployments.css delete mode 100644 frontend/src/components/pipelines-or-deployments/pipelines-or-deployments/PipelinesOrDeployments.jsx create mode 100644 frontend/src/helpers/FeatureFlagsData.js delete mode 100644 frontend/src/pages/PipelinesOrDeploymentsPage.jsx delete mode 100644 unstract/flags/src/unstract/flags/client.py create mode 100644 unstract/flags/src/unstract/flags/client/base.py create mode 100644 unstract/flags/src/unstract/flags/client/evaluation.py create mode 100644 unstract/flags/src/unstract/flags/client/flipt.py rename unstract/flags/src/unstract/flags/{ => generated}/evaluation_pb2.py (93%) rename unstract/flags/src/unstract/flags/{ => generated}/evaluation_pb2_grpc.py (99%) create mode 100644 unstract/flags/src/unstract/flags/generated/flipt_pb2.py create mode 100644 unstract/flags/src/unstract/flags/generated/flipt_pb2_grpc.py rename unstract/flags/src/unstract/flags/{protos => proto}/evaluation.proto (100%) create mode 100644 unstract/flags/src/unstract/flags/proto/flipt.proto diff --git a/backend/backend/urls.py b/backend/backend/urls.py index 725a8f7b6..9a4f70bc5 100644 --- a/backend/backend/urls.py +++ b/backend/backend/urls.py @@ -30,6 +30,7 @@ path("", include("tool_instance.urls")), path("", include("pipeline.urls")), path("", include("apps.urls")), + path("", include("feature_flag.urls")), path("workflow/", include("workflow_manager.urls")), path("platform/", include("platform_settings.urls")), path("api/", include("api.urls")), diff --git a/backend/feature_flag/urls.py b/backend/feature_flag/urls.py index c90426d1e..3952e27f9 100644 --- a/backend/feature_flag/urls.py +++ b/backend/feature_flag/urls.py @@ -3,9 +3,20 @@ This module defines the URL patterns for the feature_flags app. """ -import feature_flag.views as views from django.urls import path +from feature_flag.views import FeatureFlagViewSet +from rest_framework.urlpatterns import format_suffix_patterns -urlpatterns = [ - path("evaluate/", views.evaluate_feature_flag, name="evaluate_feature_flag"), -] +feature_flags_list = FeatureFlagViewSet.as_view( + { + "post": "evaluate", + "get": "list", + } +) + +urlpatterns = format_suffix_patterns( + [ + path("evaluate/", feature_flags_list, name="evaluate_feature_flag"), + path("flags/", feature_flags_list, name="list_feature_flags"), + ] +) diff --git a/backend/feature_flag/views.py b/backend/feature_flag/views.py index 6e155f5e3..ab318c112 100644 --- a/backend/feature_flag/views.py +++ b/backend/feature_flag/views.py @@ -6,52 +6,43 @@ import logging -from rest_framework import status -from rest_framework.decorators import api_view -from rest_framework.request import Request +from rest_framework import status, viewsets from rest_framework.response import Response - -from unstract.flags.client import EvaluationClient +from utils.request.feature_flag import check_feature_flag_status, list_all_flags logger = logging.getLogger(__name__) -@api_view(["POST"]) -def evaluate_feature_flag(request: Request) -> Response: - """Function to evaluate the feature flag. - - To-Do: Refactor to a class based view, use serializers (DRF). +class FeatureFlagViewSet(viewsets.ViewSet): + """A simple ViewSet for evaluating feature flag.""" - Args: - request: request object + def evaluate(self, request): + try: + flag_key = request.data.get("flag_key") - Returns: - evaluate response - """ - try: - namespace_key = request.data.get("namespace_key") - flag_key = request.data.get("flag_key") - entity_id = request.data.get("entity_id") - context = request.data.get("context") + if not flag_key: + return Response( + {"message": "Request parameters are missing."}, + status=status.HTTP_400_BAD_REQUEST, + ) - if not namespace_key or not flag_key or not entity_id: + flag_enabled = check_feature_flag_status(flag_key) + return Response({"flag_status": flag_enabled}, status=status.HTTP_200_OK) + except Exception as e: + logger.error("No response from server: %s", e) return Response( - {"message": "Request paramteres are missing."}, - status=status.HTTP_400_BAD_REQUEST, + {"message": "No response from server"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) - evaluation_client = EvaluationClient() - response = evaluation_client.boolean_evaluate_feature_flag( - namespace_key=namespace_key, - flag_key=flag_key, - entity_id=entity_id, - context=context, - ) - - return Response({"enabled": response}, status=status.HTTP_200_OK) - except Exception as e: - logger.error("No response from server: %s", e) - return Response( - {"message": "No response from server"}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR, - ) + def list(self, request): + try: + namespace_key = request.query_params.get("namespace", "default") + feature_flags = list_all_flags(namespace_key) + return Response({"feature_flags": feature_flags}, status=status.HTTP_200_OK) + except Exception as e: + logger.error("No response from server: %s", e) + return Response( + {"message": "No response from server"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) diff --git a/backend/utils/request/feature_flag.py b/backend/utils/request/feature_flag.py index a62d5a790..d258b377e 100644 --- a/backend/utils/request/feature_flag.py +++ b/backend/utils/request/feature_flag.py @@ -2,7 +2,8 @@ from typing import Optional -from unstract.flags.client import EvaluationClient +from unstract.flags.client.evaluation import EvaluationClient +from unstract.flags.client.flipt import FliptClient def check_feature_flag_status( @@ -38,3 +39,17 @@ def check_feature_flag_status( except Exception as e: print(f"Error: {str(e)}") return False + + +def list_all_flags( + namespace_key: str, +) -> dict: + try: + flipt_client = FliptClient() + response = flipt_client.list_feature_flags( + namespace_key=namespace_key, + ) + return response + except Exception as e: + print(f"Error: {str(e)}") + return {} diff --git a/frontend/src/assets/index.js b/frontend/src/assets/index.js index eb10ae3fc..5115b0663 100644 --- a/frontend/src/assets/index.js +++ b/frontend/src/assets/index.js @@ -31,6 +31,7 @@ import { ReactComponent as OrgSelection } from "./org-selection.svg"; import { ReactComponent as RedGradCircle } from "./red-grad-circle.svg"; import { ReactComponent as YellowGradCircle } from "./yellow-grad-circle.svg"; import { ReactComponent as ExportToolIcon } from "./export-tool.svg"; +import { ReactComponent as PlaceholderImg } from "./placeholder.svg"; export { SunIcon, @@ -66,4 +67,5 @@ export { RedGradCircle, YellowGradCircle, ExportToolIcon, + PlaceholderImg, }; diff --git a/frontend/src/assets/placeholder.svg b/frontend/src/assets/placeholder.svg new file mode 100644 index 000000000..fc76cc7a7 --- /dev/null +++ b/frontend/src/assets/placeholder.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/components/navigations/side-nav-bar/SideNavBar.jsx b/frontend/src/components/navigations/side-nav-bar/SideNavBar.jsx index 9c1add0ce..2e881a32b 100644 --- a/frontend/src/components/navigations/side-nav-bar/SideNavBar.jsx +++ b/frontend/src/components/navigations/side-nav-bar/SideNavBar.jsx @@ -7,7 +7,6 @@ const { Sider } = Layout; import Workflows from "../../../assets/Workflows.svg"; import apiDeploy from "../../../assets/api-deployments.svg"; -import appdev from "../../../assets/appdev.svg"; import CustomTools from "../../../assets/custom-tools-icon.svg"; import EmbeddingIcon from "../../../assets/embedding.svg"; import etl from "../../../assets/etl.svg"; @@ -18,10 +17,17 @@ import VectorDbIcon from "../../../assets/vector-db.svg"; import TextExtractorIcon from "../../../assets/text-extractor.svg"; import { useSessionStore } from "../../../store/session-store"; +let getMenuItem; +try { + getMenuItem = require("../../../plugins/app-deployments/app-deployment-components/helpers/getMenuItem"); +} catch (err) { + // Plugin unavailable. +} + const SideNavBar = ({ collapsed }) => { const navigate = useNavigate(); const { sessionDetails } = useSessionStore(); - const { orgName } = sessionDetails; + const { orgName, flags } = sessionDetails; const data = [ { @@ -36,15 +42,6 @@ const SideNavBar = ({ collapsed }) => { path: `/${orgName}/api`, active: window.location.pathname.startsWith(`/${orgName}/api`), }, - { - id: 1.2, - title: "App Deployments", - description: "Standalone unstructured data apps", - icon: BranchesOutlined, - image: appdev, - path: `/${orgName}/app`, - active: window.location.pathname.startsWith(`/${orgName}/app`), - }, { id: 1.3, title: "ETL Pipelines", @@ -147,6 +144,10 @@ const SideNavBar = ({ collapsed }) => { }, ]; + if (getMenuItem && flags.app_deployment) { + data[0].subMenu.splice(1, 0, getMenuItem.default(orgName)); + } + return ( { - const { Option } = Select; - const { TextArea } = Input; - - return ( - setOpen(false)} - onCancel={() => setOpen(false)} - okText="Save and Deploy" - okButtonProps={{ style: { background: "#092C4C" } }} - width={800} - closable={true} - maskClosable={false} - > -
-
- Project Name - -
- Workflow - -
- Frequency of runs -