Team Ai
Apppublic

Underground-Digital/Workflow-Engine

sourceHugging Faceupdated 2y agoView on Hugging Face
0likes
datasets.py749 linesDownload Raw Back to datasets
1import flask_restful2from flask import request3from flask_login import current_user4from flask_restful import Resource, marshal, marshal_with, reqparse5from werkzeug.exceptions import Forbidden, NotFound6 7import services8from configs import dify_config9from controllers.console import api10from controllers.console.apikey import api_key_fields, api_key_list11from controllers.console.app.error import ProviderNotInitializeError12from controllers.console.datasets.error import DatasetInUseError, DatasetNameDuplicateError, IndexingEstimateError13from controllers.console.wraps import account_initialization_required, setup_required14from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError15from core.indexing_runner import IndexingRunner16from core.model_runtime.entities.model_entities import ModelType17from core.provider_manager import ProviderManager18from core.rag.datasource.vdb.vector_type import VectorType19from core.rag.extractor.entity.extract_setting import ExtractSetting20from core.rag.retrieval.retrieval_methods import RetrievalMethod21from extensions.ext_database import db22from fields.app_fields import related_app_list23from fields.dataset_fields import dataset_detail_fields, dataset_query_detail_fields24from fields.document_fields import document_status_fields25from libs.login import login_required26from models import ApiToken, Dataset, Document, DocumentSegment, UploadFile27from models.dataset import DatasetPermissionEnum28from services.dataset_service import DatasetPermissionService, DatasetService, DocumentService29 30 31def _validate_name(name):32    if not name or len(name) < 1 or len(name) > 40:33        raise ValueError("Name must be between 1 to 40 characters.")34    return name35 36 37def _validate_description_length(description):38    if len(description) > 400:39        raise ValueError("Description cannot exceed 400 characters.")40    return description41 42 43class DatasetListApi(Resource):44    @setup_required45    @login_required46    @account_initialization_required47    def get(self):48        page = request.args.get("page", default=1, type=int)49        limit = request.args.get("limit", default=20, type=int)50        ids = request.args.getlist("ids")51        # provider = request.args.get("provider", default="vendor")52        search = request.args.get("keyword", default=None, type=str)53        tag_ids = request.args.getlist("tag_ids")54 55        if ids:56            datasets, total = DatasetService.get_datasets_by_ids(ids, current_user.current_tenant_id)57        else:58            datasets, total = DatasetService.get_datasets(59                page, limit, current_user.current_tenant_id, current_user, search, tag_ids60            )61 62        # check embedding setting63        provider_manager = ProviderManager()64        configurations = provider_manager.get_configurations(tenant_id=current_user.current_tenant_id)65 66        embedding_models = configurations.get_models(model_type=ModelType.TEXT_EMBEDDING, only_active=True)67 68        model_names = []69        for embedding_model in embedding_models:70            model_names.append(f"{embedding_model.model}:{embedding_model.provider.provider}")71 72        data = marshal(datasets, dataset_detail_fields)73        for item in data:74            if item["indexing_technique"] == "high_quality":75                item_model = f"{item['embedding_model']}:{item['embedding_model_provider']}"76                if item_model in model_names:77                    item["embedding_available"] = True78                else:79                    item["embedding_available"] = False80            else:81                item["embedding_available"] = True82 83            if item.get("permission") == "partial_members":84                part_users_list = DatasetPermissionService.get_dataset_partial_member_list(item["id"])85                item.update({"partial_member_list": part_users_list})86            else:87                item.update({"partial_member_list": []})88 89        response = {"data": data, "has_more": len(datasets) == limit, "limit": limit, "total": total, "page": page}90        return response, 20091 92    @setup_required93    @login_required94    @account_initialization_required95    def post(self):96        parser = reqparse.RequestParser()97        parser.add_argument(98            "name",99            nullable=False,100            required=True,101            help="type is required. Name must be between 1 to 40 characters.",102            type=_validate_name,103        )104        parser.add_argument(105            "description",106            type=str,107            nullable=True,108            required=False,109            default="",110        )111        parser.add_argument(112            "indexing_technique",113            type=str,114            location="json",115            choices=Dataset.INDEXING_TECHNIQUE_LIST,116            nullable=True,117            help="Invalid indexing technique.",118        )119        parser.add_argument(120            "external_knowledge_api_id",121            type=str,122            nullable=True,123            required=False,124        )125        parser.add_argument(126            "provider",127            type=str,128            nullable=True,129            choices=Dataset.PROVIDER_LIST,130            required=False,131            default="vendor",132        )133        parser.add_argument(134            "external_knowledge_id",135            type=str,136            nullable=True,137            required=False,138        )139        args = parser.parse_args()140 141        # The role of the current user in the ta table must be admin, owner, or editor, or dataset_operator142        if not current_user.is_dataset_editor:143            raise Forbidden()144 145        try:146            dataset = DatasetService.create_empty_dataset(147                tenant_id=current_user.current_tenant_id,148                name=args["name"],149                description=args["description"],150                indexing_technique=args["indexing_technique"],151                account=current_user,152                permission=DatasetPermissionEnum.ONLY_ME,153                provider=args["provider"],154                external_knowledge_api_id=args["external_knowledge_api_id"],155                external_knowledge_id=args["external_knowledge_id"],156            )157        except services.errors.dataset.DatasetNameDuplicateError:158            raise DatasetNameDuplicateError()159 160        return marshal(dataset, dataset_detail_fields), 201161 162 163class DatasetApi(Resource):164    @setup_required165    @login_required166    @account_initialization_required167    def get(self, dataset_id):168        dataset_id_str = str(dataset_id)169        dataset = DatasetService.get_dataset(dataset_id_str)170        if dataset is None:171            raise NotFound("Dataset not found.")172        try:173            DatasetService.check_dataset_permission(dataset, current_user)174        except services.errors.account.NoPermissionError as e:175            raise Forbidden(str(e))176        data = marshal(dataset, dataset_detail_fields)177        if data.get("permission") == "partial_members":178            part_users_list = DatasetPermissionService.get_dataset_partial_member_list(dataset_id_str)179            data.update({"partial_member_list": part_users_list})180 181        # check embedding setting182        provider_manager = ProviderManager()183        configurations = provider_manager.get_configurations(tenant_id=current_user.current_tenant_id)184 185        embedding_models = configurations.get_models(model_type=ModelType.TEXT_EMBEDDING, only_active=True)186 187        model_names = []188        for embedding_model in embedding_models:189            model_names.append(f"{embedding_model.model}:{embedding_model.provider.provider}")190 191        if data["indexing_technique"] == "high_quality":192            item_model = f"{data['embedding_model']}:{data['embedding_model_provider']}"193            if item_model in model_names:194                data["embedding_available"] = True195            else:196                data["embedding_available"] = False197        else:198            data["embedding_available"] = True199 200        if data.get("permission") == "partial_members":201            part_users_list = DatasetPermissionService.get_dataset_partial_member_list(dataset_id_str)202            data.update({"partial_member_list": part_users_list})203 204        return data, 200205 206    @setup_required207    @login_required208    @account_initialization_required209    def patch(self, dataset_id):210        dataset_id_str = str(dataset_id)211        dataset = DatasetService.get_dataset(dataset_id_str)212        if dataset is None:213            raise NotFound("Dataset not found.")214 215        parser = reqparse.RequestParser()216        parser.add_argument(217            "name",218            nullable=False,219            help="type is required. Name must be between 1 to 40 characters.",220            type=_validate_name,221        )222        parser.add_argument("description", location="json", store_missing=False, type=_validate_description_length)223        parser.add_argument(224            "indexing_technique",225            type=str,226            location="json",227            choices=Dataset.INDEXING_TECHNIQUE_LIST,228            nullable=True,229            help="Invalid indexing technique.",230        )231        parser.add_argument(232            "permission",233            type=str,234            location="json",235            choices=(DatasetPermissionEnum.ONLY_ME, DatasetPermissionEnum.ALL_TEAM, DatasetPermissionEnum.PARTIAL_TEAM),236            help="Invalid permission.",237        )238        parser.add_argument("embedding_model", type=str, location="json", help="Invalid embedding model.")239        parser.add_argument(240            "embedding_model_provider", type=str, location="json", help="Invalid embedding model provider."241        )242        parser.add_argument("retrieval_model", type=dict, location="json", help="Invalid retrieval model.")243        parser.add_argument("partial_member_list", type=list, location="json", help="Invalid parent user list.")244 245        parser.add_argument(246            "external_retrieval_model",247            type=dict,248            required=False,249            nullable=True,250            location="json",251            help="Invalid external retrieval model.",252        )253 254        parser.add_argument(255            "external_knowledge_id",256            type=str,257            required=False,258            nullable=True,259            location="json",260            help="Invalid external knowledge id.",261        )262 263        parser.add_argument(264            "external_knowledge_api_id",265            type=str,266            required=False,267            nullable=True,268            location="json",269            help="Invalid external knowledge api id.",270        )271        args = parser.parse_args()272        data = request.get_json()273 274        # check embedding model setting275        if data.get("indexing_technique") == "high_quality":276            DatasetService.check_embedding_model_setting(277                dataset.tenant_id, data.get("embedding_model_provider"), data.get("embedding_model")278            )279 280        # The role of the current user in the ta table must be admin, owner, editor, or dataset_operator281        DatasetPermissionService.check_permission(282            current_user, dataset, data.get("permission"), data.get("partial_member_list")283        )284 285        dataset = DatasetService.update_dataset(dataset_id_str, args, current_user)286 287        if dataset is None:288            raise NotFound("Dataset not found.")289 290        result_data = marshal(dataset, dataset_detail_fields)291        tenant_id = current_user.current_tenant_id292 293        if data.get("partial_member_list") and data.get("permission") == "partial_members":294            DatasetPermissionService.update_partial_member_list(295                tenant_id, dataset_id_str, data.get("partial_member_list")296            )297        # clear partial member list when permission is only_me or all_team_members298        elif (299            data.get("permission") == DatasetPermissionEnum.ONLY_ME300            or data.get("permission") == DatasetPermissionEnum.ALL_TEAM301        ):302            DatasetPermissionService.clear_partial_member_list(dataset_id_str)303 304        partial_member_list = DatasetPermissionService.get_dataset_partial_member_list(dataset_id_str)305        result_data.update({"partial_member_list": partial_member_list})306 307        return result_data, 200308 309    @setup_required310    @login_required311    @account_initialization_required312    def delete(self, dataset_id):313        dataset_id_str = str(dataset_id)314 315        # The role of the current user in the ta table must be admin, owner, or editor316        if not current_user.is_editor or current_user.is_dataset_operator:317            raise Forbidden()318 319        try:320            if DatasetService.delete_dataset(dataset_id_str, current_user):321                DatasetPermissionService.clear_partial_member_list(dataset_id_str)322                return {"result": "success"}, 204323            else:324                raise NotFound("Dataset not found.")325        except services.errors.dataset.DatasetInUseError:326            raise DatasetInUseError()327 328 329class DatasetUseCheckApi(Resource):330    @setup_required331    @login_required332    @account_initialization_required333    def get(self, dataset_id):334        dataset_id_str = str(dataset_id)335 336        dataset_is_using = DatasetService.dataset_use_check(dataset_id_str)337        return {"is_using": dataset_is_using}, 200338 339 340class DatasetQueryApi(Resource):341    @setup_required342    @login_required343    @account_initialization_required344    def get(self, dataset_id):345        dataset_id_str = str(dataset_id)346        dataset = DatasetService.get_dataset(dataset_id_str)347        if dataset is None:348            raise NotFound("Dataset not found.")349 350        try:351            DatasetService.check_dataset_permission(dataset, current_user)352        except services.errors.account.NoPermissionError as e:353            raise Forbidden(str(e))354 355        page = request.args.get("page", default=1, type=int)356        limit = request.args.get("limit", default=20, type=int)357 358        dataset_queries, total = DatasetService.get_dataset_queries(dataset_id=dataset.id, page=page, per_page=limit)359 360        response = {361            "data": marshal(dataset_queries, dataset_query_detail_fields),362            "has_more": len(dataset_queries) == limit,363            "limit": limit,364            "total": total,365            "page": page,366        }367        return response, 200368 369 370class DatasetIndexingEstimateApi(Resource):371    @setup_required372    @login_required373    @account_initialization_required374    def post(self):375        parser = reqparse.RequestParser()376        parser.add_argument("info_list", type=dict, required=True, nullable=True, location="json")377        parser.add_argument("process_rule", type=dict, required=True, nullable=True, location="json")378        parser.add_argument(379            "indexing_technique",380            type=str,381            required=True,382            choices=Dataset.INDEXING_TECHNIQUE_LIST,383            nullable=True,384            location="json",385        )386        parser.add_argument("doc_form", type=str, default="text_model", required=False, nullable=False, location="json")387        parser.add_argument("dataset_id", type=str, required=False, nullable=False, location="json")388        parser.add_argument(389            "doc_language", type=str, default="English", required=False, nullable=False, location="json"390        )391        args = parser.parse_args()392        # validate args393        DocumentService.estimate_args_validate(args)394        extract_settings = []395        if args["info_list"]["data_source_type"] == "upload_file":396            file_ids = args["info_list"]["file_info_list"]["file_ids"]397            file_details = (398                db.session.query(UploadFile)399                .filter(UploadFile.tenant_id == current_user.current_tenant_id, UploadFile.id.in_(file_ids))400                .all()401            )402 403            if file_details is None:404                raise NotFound("File not found.")405 406            if file_details:407                for file_detail in file_details:408                    extract_setting = ExtractSetting(409                        datasource_type="upload_file", upload_file=file_detail, document_model=args["doc_form"]410                    )411                    extract_settings.append(extract_setting)412        elif args["info_list"]["data_source_type"] == "notion_import":413            notion_info_list = args["info_list"]["notion_info_list"]414            for notion_info in notion_info_list:415                workspace_id = notion_info["workspace_id"]416                for page in notion_info["pages"]:417                    extract_setting = ExtractSetting(418                        datasource_type="notion_import",419                        notion_info={420                            "notion_workspace_id": workspace_id,421                            "notion_obj_id": page["page_id"],422                            "notion_page_type": page["type"],423                            "tenant_id": current_user.current_tenant_id,424                        },425                        document_model=args["doc_form"],426                    )427                    extract_settings.append(extract_setting)428        elif args["info_list"]["data_source_type"] == "website_crawl":429            website_info_list = args["info_list"]["website_info_list"]430            for url in website_info_list["urls"]:431                extract_setting = ExtractSetting(432                    datasource_type="website_crawl",433                    website_info={434                        "provider": website_info_list["provider"],435                        "job_id": website_info_list["job_id"],436                        "url": url,437                        "tenant_id": current_user.current_tenant_id,438                        "mode": "crawl",439                        "only_main_content": website_info_list["only_main_content"],440                    },441                    document_model=args["doc_form"],442                )443                extract_settings.append(extract_setting)444        else:445            raise ValueError("Data source type not support")446        indexing_runner = IndexingRunner()447        try:448            response = indexing_runner.indexing_estimate(449                current_user.current_tenant_id,450                extract_settings,451                args["process_rule"],452                args["doc_form"],453                args["doc_language"],454                args["dataset_id"],455                args["indexing_technique"],456            )457        except LLMBadRequestError:458            raise ProviderNotInitializeError(459                "No Embedding Model available. Please configure a valid provider " "in the Settings -> Model Provider."460            )461        except ProviderTokenNotInitError as ex:462            raise ProviderNotInitializeError(ex.description)463        except Exception as e:464            raise IndexingEstimateError(str(e))465 466        return response, 200467 468 469class DatasetRelatedAppListApi(Resource):470    @setup_required471    @login_required472    @account_initialization_required473    @marshal_with(related_app_list)474    def get(self, dataset_id):475        dataset_id_str = str(dataset_id)476        dataset = DatasetService.get_dataset(dataset_id_str)477        if dataset is None:478            raise NotFound("Dataset not found.")479 480        try:481            DatasetService.check_dataset_permission(dataset, current_user)482        except services.errors.account.NoPermissionError as e:483            raise Forbidden(str(e))484 485        app_dataset_joins = DatasetService.get_related_apps(dataset.id)486 487        related_apps = []488        for app_dataset_join in app_dataset_joins:489            app_model = app_dataset_join.app490            if app_model:491                related_apps.append(app_model)492 493        return {"data": related_apps, "total": len(related_apps)}, 200494 495 496class DatasetIndexingStatusApi(Resource):497    @setup_required498    @login_required499    @account_initialization_required500    def get(self, dataset_id):501        dataset_id = str(dataset_id)502        documents = (503            db.session.query(Document)504            .filter(Document.dataset_id == dataset_id, Document.tenant_id == current_user.current_tenant_id)505            .all()506        )507        documents_status = []508        for document in documents:509            completed_segments = DocumentSegment.query.filter(510                DocumentSegment.completed_at.isnot(None),511                DocumentSegment.document_id == str(document.id),512                DocumentSegment.status != "re_segment",513            ).count()514            total_segments = DocumentSegment.query.filter(515                DocumentSegment.document_id == str(document.id), DocumentSegment.status != "re_segment"516            ).count()517            document.completed_segments = completed_segments518            document.total_segments = total_segments519            documents_status.append(marshal(document, document_status_fields))520        data = {"data": documents_status}521        return data522 523 524class DatasetApiKeyApi(Resource):525    max_keys = 10526    token_prefix = "dataset-"527    resource_type = "dataset"528 529    @setup_required530    @login_required531    @account_initialization_required532    @marshal_with(api_key_list)533    def get(self):534        keys = (535            db.session.query(ApiToken)536            .filter(ApiToken.type == self.resource_type, ApiToken.tenant_id == current_user.current_tenant_id)537            .all()538        )539        return {"items": keys}540 541    @setup_required542    @login_required543    @account_initialization_required544    @marshal_with(api_key_fields)545    def post(self):546        # The role of the current user in the ta table must be admin or owner547        if not current_user.is_admin_or_owner:548            raise Forbidden()549 550        current_key_count = (551            db.session.query(ApiToken)552            .filter(ApiToken.type == self.resource_type, ApiToken.tenant_id == current_user.current_tenant_id)553            .count()554        )555 556        if current_key_count >= self.max_keys:557            flask_restful.abort(558                400,559                message=f"Cannot create more than {self.max_keys} API keys for this resource type.",560                code="max_keys_exceeded",561            )562 563        key = ApiToken.generate_api_key(self.token_prefix, 24)564        api_token = ApiToken()565        api_token.tenant_id = current_user.current_tenant_id566        api_token.token = key567        api_token.type = self.resource_type568        db.session.add(api_token)569        db.session.commit()570        return api_token, 200571 572 573class DatasetApiDeleteApi(Resource):574    resource_type = "dataset"575 576    @setup_required577    @login_required578    @account_initialization_required579    def delete(self, api_key_id):580        api_key_id = str(api_key_id)581 582        # The role of the current user in the ta table must be admin or owner583        if not current_user.is_admin_or_owner:584            raise Forbidden()585 586        key = (587            db.session.query(ApiToken)588            .filter(589                ApiToken.tenant_id == current_user.current_tenant_id,590                ApiToken.type == self.resource_type,591                ApiToken.id == api_key_id,592            )593            .first()594        )595 596        if key is None:597            flask_restful.abort(404, message="API key not found")598 599        db.session.query(ApiToken).filter(ApiToken.id == api_key_id).delete()600        db.session.commit()601 602        return {"result": "success"}, 204603 604 605class DatasetApiBaseUrlApi(Resource):606    @setup_required607    @login_required608    @account_initialization_required609    def get(self):610        return {"api_base_url": (dify_config.SERVICE_API_URL or request.host_url.rstrip("/")) + "/v1"}611 612 613class DatasetRetrievalSettingApi(Resource):614    @setup_required615    @login_required616    @account_initialization_required617    def get(self):618        vector_type = dify_config.VECTOR_STORE619        match vector_type:620            case (621                VectorType.MILVUS622                | VectorType.RELYT623                | VectorType.PGVECTOR624                | VectorType.TIDB_VECTOR625                | VectorType.CHROMA626                | VectorType.TENCENT627                | VectorType.PGVECTO_RS628                | VectorType.BAIDU629                | VectorType.VIKINGDB630                | VectorType.UPSTASH631                | VectorType.OCEANBASE632            ):633                return {"retrieval_method": [RetrievalMethod.SEMANTIC_SEARCH.value]}634            case (635                VectorType.QDRANT636                | VectorType.WEAVIATE637                | VectorType.OPENSEARCH638                | VectorType.ANALYTICDB639                | VectorType.MYSCALE640                | VectorType.ORACLE641                | VectorType.ELASTICSEARCH642                | VectorType.PGVECTOR643                | VectorType.TIDB_ON_QDRANT644                | VectorType.LINDORM645                | VectorType.COUCHBASE646            ):647                return {648                    "retrieval_method": [649                        RetrievalMethod.SEMANTIC_SEARCH.value,650                        RetrievalMethod.FULL_TEXT_SEARCH.value,651                        RetrievalMethod.HYBRID_SEARCH.value,652                    ]653                }654            case _:655                raise ValueError(f"Unsupported vector db type {vector_type}.")656 657 658class DatasetRetrievalSettingMockApi(Resource):659    @setup_required660    @login_required661    @account_initialization_required662    def get(self, vector_type):663        match vector_type:664            case (665                VectorType.MILVUS666                | VectorType.RELYT667                | VectorType.TIDB_VECTOR668                | VectorType.CHROMA669                | VectorType.TENCENT670                | VectorType.PGVECTO_RS671                | VectorType.BAIDU672                | VectorType.VIKINGDB673                | VectorType.UPSTASH674                | VectorType.OCEANBASE675            ):676                return {"retrieval_method": [RetrievalMethod.SEMANTIC_SEARCH.value]}677            case (678                VectorType.QDRANT679                | VectorType.WEAVIATE680                | VectorType.OPENSEARCH681                | VectorType.ANALYTICDB682                | VectorType.MYSCALE683                | VectorType.ORACLE684                | VectorType.ELASTICSEARCH685                | VectorType.COUCHBASE686                | VectorType.PGVECTOR687                | VectorType.LINDORM688            ):689                return {690                    "retrieval_method": [691                        RetrievalMethod.SEMANTIC_SEARCH.value,692                        RetrievalMethod.FULL_TEXT_SEARCH.value,693                        RetrievalMethod.HYBRID_SEARCH.value,694                    ]695                }696            case _:697                raise ValueError(f"Unsupported vector db type {vector_type}.")698 699 700class DatasetErrorDocs(Resource):701    @setup_required702    @login_required703    @account_initialization_required704    def get(self, dataset_id):705        dataset_id_str = str(dataset_id)706        dataset = DatasetService.get_dataset(dataset_id_str)707        if dataset is None:708            raise NotFound("Dataset not found.")709        results = DocumentService.get_error_documents_by_dataset_id(dataset_id_str)710 711        return {"data": [marshal(item, document_status_fields) for item in results], "total": len(results)}, 200712 713 714class DatasetPermissionUserListApi(Resource):715    @setup_required716    @login_required717    @account_initialization_required718    def get(self, dataset_id):719        dataset_id_str = str(dataset_id)720        dataset = DatasetService.get_dataset(dataset_id_str)721        if dataset is None:722            raise NotFound("Dataset not found.")723        try:724            DatasetService.check_dataset_permission(dataset, current_user)725        except services.errors.account.NoPermissionError as e:726            raise Forbidden(str(e))727 728        partial_members_list = DatasetPermissionService.get_dataset_partial_member_list(dataset_id_str)729 730        return {731            "data": partial_members_list,732        }, 200733 734 735api.add_resource(DatasetListApi, "/datasets")736api.add_resource(DatasetApi, "/datasets/<uuid:dataset_id>")737api.add_resource(DatasetUseCheckApi, "/datasets/<uuid:dataset_id>/use-check")738api.add_resource(DatasetQueryApi, "/datasets/<uuid:dataset_id>/queries")739api.add_resource(DatasetErrorDocs, "/datasets/<uuid:dataset_id>/error-docs")740api.add_resource(DatasetIndexingEstimateApi, "/datasets/indexing-estimate")741api.add_resource(DatasetRelatedAppListApi, "/datasets/<uuid:dataset_id>/related-apps")742api.add_resource(DatasetIndexingStatusApi, "/datasets/<uuid:dataset_id>/indexing-status")743api.add_resource(DatasetApiKeyApi, "/datasets/api-keys")744api.add_resource(DatasetApiDeleteApi, "/datasets/api-keys/<uuid:api_key_id>")745api.add_resource(DatasetApiBaseUrlApi, "/datasets/api-base-info")746api.add_resource(DatasetRetrievalSettingApi, "/datasets/retrieval-setting")747api.add_resource(DatasetRetrievalSettingMockApi, "/datasets/retrieval-setting/<string:vector_type>")748api.add_resource(DatasetPermissionUserListApi, "/datasets/<uuid:dataset_id>/permission-part-users")749