Sfoglia il codice sorgente

Merge branch 'refs/heads/1.1.3-lxg' into 1.1.3-master

liangxunge 3 mesi fa
parent
commit
03644c3a84

+ 1 - 1
api/controllers/console/__init__.py

@@ -43,7 +43,7 @@ api.add_resource(AppImportConfirmApi, "/apps/imports/<string:import_id>/confirm"
 api.add_resource(AppImportCheckDependenciesApi, "/apps/imports/<string:app_id>/check-dependencies")
 
 # Import other controllers
-from . import admin, apikey, extension, external_application, feature, ping, setup, version
+from . import admin, apikey, extension, external_application, feature, intention, ping, setup, version
 
 # Import app controllers
 from .app import (

+ 516 - 0
api/controllers/console/intention.py

@@ -0,0 +1,516 @@
+import logging
+
+from flask import request
+from flask_restful import Resource, marshal, marshal_with, reqparse
+from werkzeug.exceptions import Forbidden, NotFound
+
+from controllers.console import api
+from controllers.console.wraps import account_initialization_required, setup_required
+from fields.intention_fields import (
+    intention_corpus_detail_fields,
+    intention_corpus_similarity_question_fields,
+    intention_detail_fields,
+    intention_keyword_detail_fields,
+    intention_keyword_fields,
+    intention_page_fields,
+    intention_type_detail_fields,
+    intention_type_page_fields,
+)
+from libs.login import login_required
+from services.intention_service import (
+    IntentionCorpusService,
+    IntentionCorpusSimilarityQuestionService,
+    IntentionKeywordService,
+    IntentionService,
+    IntentionTypeService,
+)
+
+
+class IntentionListApi(Resource):
+
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def get(self):
+        page = request.args.get("page", default=1, type=int)
+        limit = request.args.get("limit", default=20, type=int)
+        type_id = request.args.get("type_id", default=None, type=str)
+        name_search = request.args.get("name_search", default=None, type=str)
+        intentions, total = IntentionService.get_intentions(page, limit, type_id, name_search)
+        data = marshal(intentions, intention_page_fields)
+        response = {"data": data, "has_more": len(intentions) == limit, "limit": limit,
+                    "total": total, "page": page}
+        return response, 200
+
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def post(self):
+        parser = reqparse.RequestParser()
+        parser.add_argument(
+            "name",
+            nullable=False,
+            required=True,
+            help="type is required. Name must be between 1 to 40 characters.",
+        )
+        parser.add_argument(
+            "type_id",
+            nullable=False,
+            required=True,
+            help="type is required.",
+        )
+        args = parser.parse_args()
+        intention = IntentionService.save_intention(args)
+        response = marshal(intention, intention_detail_fields)
+        return response, 200
+
+class IntentionApi(Resource):
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def get(self, intention_id):
+        intention = IntentionService.get_intention(intention_id)
+        return marshal(intention, intention_detail_fields)
+
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def patch(self, intention_id):
+        parser = reqparse.RequestParser()
+        parser.add_argument(
+            "name",
+            nullable=False,
+            required=True,
+            help="type is required. Name must be between 1 to 40 characters.",
+        )
+        parser.add_argument(
+            "type_id",
+            nullable=False,
+            required=True,
+            help="type is required.",
+        )
+        args = parser.parse_args()
+        intention = IntentionService.update_intention(intention_id, args)
+        response = marshal(intention, intention_detail_fields)
+        return response, 200
+
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def delete(self, intention_id):
+        IntentionService.delete_intention(intention_id)
+        return 200
+
+class IntentionTypeListApi(Resource):
+
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def get(self):
+        page = request.args.get("page", default=1, type=int)
+        limit = request.args.get("limit", default=20, type=int)
+        search = request.args.get("search", default=None, type=str)
+        intention_types, total = IntentionTypeService.get_intention_types(page, limit, search)
+        data = marshal(intention_types, intention_type_page_fields)
+        response = {"data": data, "has_more": len(intention_types) == limit, "limit": limit,
+                    "total": total, "page": page}
+        return response, 200
+
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def post(self):
+        parser = reqparse.RequestParser()
+        parser.add_argument(
+            "name",
+            nullable=False,
+            required=True,
+            help="type is required. Name must be between 1 to 40 characters.",
+        )
+        args = parser.parse_args()
+        intention_type = IntentionTypeService.save_intention_type(args)
+        response = marshal(intention_type, intention_type_detail_fields)
+        return response, 200
+
+class IntentionTypeApi(Resource):
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def get(self, intention_type_id):
+        intention_type = IntentionTypeService.get_intention_type(intention_type_id)
+        return marshal(intention_type, intention_type_detail_fields)
+
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def patch(self, intention_type_id):
+        parser = reqparse.RequestParser()
+        parser.add_argument(
+            "name",
+            nullable=False,
+            required=True,
+            help="type is required. Name must be between 1 to 40 characters.",
+        )
+        args = parser.parse_args()
+        intention_type = IntentionTypeService.update_intention_type(intention_type_id, args)
+        return marshal(intention_type, intention_type_detail_fields), 200
+
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def delete(self, intention_type_id):
+        IntentionTypeService.delete_intention_type(intention_type_id)
+        return 200
+
+class IntentionKeywordListApi(Resource):
+    @setup_required
+    @login_required
+    @account_initialization_required
+    @marshal_with(intention_keyword_fields)
+    def get(self, intention_id):
+        search = request.args.get("search", default=None, type=str)
+        intention = IntentionService.get_intention(intention_id)
+        if not intention:
+            raise NotFound("Intention not found")
+        intention_keywords = IntentionKeywordService.get_intention_keywords(intention_id, search)
+        return intention_keywords, 200
+
+    @setup_required
+    @login_required
+    @account_initialization_required
+    @marshal_with(intention_keyword_detail_fields)
+    def post(self, intention_id):
+        parser = reqparse.RequestParser()
+        parser.add_argument(
+            "name",
+            nullable=False,
+            required=True,
+            help="type is required. Name must be between 1 to 40 characters.",
+        )
+        args = parser.parse_args()
+        intention = IntentionService.get_intention(intention_id)
+        if not intention:
+            raise NotFound("Intention not found")
+        intention_keyword = IntentionKeywordService.save_intention_keyword(intention_id, args)
+        return intention_keyword, 200
+
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def delete(self, intention_id):
+        intention = IntentionService.get_intention(intention_id)
+        if not intention:
+            raise NotFound("Intention not found")
+
+        IntentionKeywordService.delete_intention_keywords_by_intention_id(intention_id)
+        return 200
+
+class IntentionKeywordApi(Resource):
+
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def get(self, intention_keyword_id):
+        intention_keyword = IntentionKeywordService.get_intention_keyword(intention_keyword_id)
+        if not intention_keyword:
+            return {}, 200
+        return marshal(intention_keyword, intention_keyword_detail_fields), 200
+
+    @setup_required
+    @login_required
+    @account_initialization_required
+    @marshal_with(intention_keyword_detail_fields)
+    def patch(self, intention_keyword_id):
+        parser = reqparse.RequestParser()
+        parser.add_argument(
+            "name",
+            nullable=False,
+            required=True,
+            help="type is required. Name must be between 1 to 40 characters.",
+        )
+        parser.add_argument(
+            "intention_id",
+            nullable=False,
+            required=True,
+            help="type is required.",
+        )
+        args = parser.parse_args()
+        intention_keyword = IntentionKeywordService.update_intention_keyword(intention_keyword_id, args)
+        return intention_keyword, 200
+
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def delete(self, intention_keyword_id):
+        IntentionKeywordService.delete_intention_keyword(intention_keyword_id)
+        return 200
+
+class IntentionKeywordBatchApi(Resource):
+
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def post(self):
+        parser = reqparse.RequestParser()
+        parser.add_argument(
+            "method",
+            nullable=False,
+            required=True,
+            help="method is required.",
+            choices=["create", "update", "delete"],
+            type=str,
+            location="json",
+        )
+        parser.add_argument(
+            "delete_data",
+            nullable=False,
+            required=True,
+            help="delete_data is required.",
+            type=list,
+            location="json",
+        )
+        args = parser.parse_args()
+        logging.info(args)
+
+        method = args["method"]
+        if method == "delete":
+            intention_keyword_ids = args["delete_data"]
+            IntentionKeywordService.delete_intention_keywords(intention_keyword_ids)
+            return 200
+        else:
+            raise NotFound(f"method with name {method} not found")
+
+class IntentionCorpusListApi(Resource):
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def get(self):
+        page = request.args.get("page", default=1, type=int)
+        limit = request.args.get("limit", default=20, type=int)
+        question_search = request.args.get("question_search", default=None, type=str)
+        intention_id = request.args.get("intention_id", default=None, type=str)
+        intention_corpus, total = IntentionCorpusService.get_page_intention_corpus(
+            page, limit, question_search, intention_id)
+        data = marshal(intention_corpus, intention_corpus_detail_fields)
+        response = {"data": data, "has_more": len(intention_corpus) == limit, "limit": limit,
+                    "total": total, "page": page}
+        return response, 200
+
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def post(self):
+        parser = reqparse.RequestParser()
+        parser.add_argument(
+            "question",
+            nullable=False,
+            required=True,
+            help="type is required. Question must be between 1 to 40 characters.",
+        )
+        parser.add_argument(
+            "question_config",
+            nullable=True,
+            required=False,
+            location="json",
+        )
+        parser.add_argument(
+            "intention_id",
+            nullable=False,
+            required=True,
+            help="type is required.",
+        )
+        args = parser.parse_args()
+        intention_corpus = IntentionCorpusService.save_intention_corpus(args)
+        return marshal(intention_corpus, intention_corpus_detail_fields), 200
+
+class IntentionCorpusApi(Resource):
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def get(self, corpus_id):
+        intention_corpus = IntentionCorpusService.get_intention_corpus(corpus_id)
+        if not intention_corpus:
+            raise NotFound(f"IntentionCorpus with id {corpus_id} not found")
+        return marshal(intention_corpus, intention_corpus_detail_fields), 200
+
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def patch(self, corpus_id):
+        parser = reqparse.RequestParser()
+        parser.add_argument(
+            "question",
+            nullable=True,
+            required=False,
+            type=str,
+            location="json",
+        )
+        parser.add_argument(
+            "question_config",
+            nullable=True,
+            required=False,
+            location="json",
+        )
+        parser.add_argument(
+            "intention_id",
+            nullable=True,
+            required=False,
+            type=str,
+            location="json",
+        )
+        args = parser.parse_args()
+        intention_corpus = IntentionCorpusService.update_intention_corpus(corpus_id, args)
+        return marshal(intention_corpus, intention_corpus_detail_fields), 200
+
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def delete(self, corpus_id):
+        intention_corpus = IntentionCorpusService.get_intention_corpus(corpus_id)
+        if not intention_corpus:
+            raise NotFound(f"未发现Id未{corpus_id}的训练语料")
+
+        similarity_questions = intention_corpus.similarity_questions
+        if similarity_questions:
+            raise Forbidden(f"存在与其关联的相似问题,无法删除Id为{corpus_id}训练语料")
+
+        IntentionCorpusService.delete_intention_corpus(intention_corpus)
+        return 200
+
+class IntentionCorpusSimilarityQuestionApi(Resource):
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def get(self, corpus_id):
+        search = request.args.get("search", default=None, type=str)
+        similarity_questions = (
+            IntentionCorpusSimilarityQuestionService
+            .get_similarity_questions_by_corpus_id_like_question(corpus_id, search)
+        )
+        return marshal(similarity_questions, intention_corpus_similarity_question_fields), 200
+
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def post(self, corpus_id):
+        parser = reqparse.RequestParser()
+        parser.add_argument(
+            "question",
+            nullable=False,
+            required=True,
+            help="type is required. Question must be between 1 to 40 characters.",
+            location="json",
+        )
+        parser.add_argument(
+            "question_config",
+            nullable=True,
+            required=False,
+            location="json",
+        )
+        args = parser.parse_args()
+        intention_corpus_similarity_question = (
+            IntentionCorpusSimilarityQuestionService.save_similarity_question(corpus_id, args)
+        )
+        return marshal(intention_corpus_similarity_question, intention_corpus_similarity_question_fields), 200
+
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def delete(self, corpus_id):
+        IntentionCorpusSimilarityQuestionService.delete_similarity_question_by_corpus_id(corpus_id)
+        return 200
+
+class IntentionCorpusSimilarityQuestionUpdateAndDeleteApi(Resource):
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def patch(self, similarity_question_id):
+        parser = reqparse.RequestParser()
+        parser.add_argument(
+            "question",
+            nullable=True,
+            required=False,
+            help="type is required. Question must be between 1 to 40 characters.",
+            location="json",
+        )
+        parser.add_argument(
+            "question_config",
+            nullable=True,
+            required=False,
+            location="json",
+        )
+        parser.add_argument(
+            "corpus_id",
+            nullable=True,
+            required=False,
+            location="json",
+        )
+        args = parser.parse_args()
+
+        similarity_question = (
+            IntentionCorpusSimilarityQuestionService.update_similarity_question(similarity_question_id, args)
+        )
+        return marshal(similarity_question, intention_corpus_similarity_question_fields), 200
+
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def delete(self, similarity_question_id):
+        IntentionCorpusSimilarityQuestionService.delete_similarity_question_by_id(similarity_question_id)
+        return 200
+
+class IntentionCorpusSimilarityQuestionBatchApi(Resource):
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def post(self):
+        parser = reqparse.RequestParser()
+        parser.add_argument(
+            "method",
+            nullable=False,
+            required=True,
+            help="method is required.",
+            choices=["create", "update", "delete"],
+            type=str,
+            location="json",
+        )
+        parser.add_argument(
+            "data",
+            nullable=False,
+            required=True,
+            help="data is required.",
+            type=list,
+            location="json",
+        )
+        args = parser.parse_args()
+        logging.info(args)
+
+        method = args["method"]
+        if method == "delete":
+            similarity_question_ids = args["data"]
+            IntentionCorpusSimilarityQuestionService.delete_similarity_questions_by_ids(similarity_question_ids)
+            return 200
+        else:
+            raise NotFound(f"method with name {method} not found")
+
+api.add_resource(IntentionListApi, "/intentions")
+api.add_resource(IntentionApi, "/intentions/<uuid:intention_id>")
+api.add_resource(IntentionTypeListApi, "/intentions/types")
+api.add_resource(IntentionTypeApi, "/intentions/types/<uuid:intention_type_id>")
+api.add_resource(IntentionKeywordListApi, "/intentions/<uuid:intention_id>/keywords")
+api.add_resource(IntentionKeywordApi, "/intentions/keywords/<uuid:intention_keyword_id>")
+api.add_resource(IntentionKeywordBatchApi, "/intentions/keywords/batch")
+
+api.add_resource(IntentionCorpusListApi, "/intentions/corpus")
+api.add_resource(IntentionCorpusApi, "/intentions/corpus/<uuid:corpus_id>")
+api.add_resource(IntentionCorpusSimilarityQuestionApi, "/intentions/corpus/<uuid:corpus_id>/similarity_questions")
+
+api.add_resource(IntentionCorpusSimilarityQuestionUpdateAndDeleteApi,
+                 "/intentions/similarity_questions/<uuid:similarity_question_id>")
+api.add_resource(IntentionCorpusSimilarityQuestionBatchApi, "/intentions/similarity_questions/batch")
+
+
+
+
+
+

+ 95 - 0
api/fields/intention_fields.py

@@ -0,0 +1,95 @@
+from flask_restful import fields  # type: ignore
+
+from libs.helper import TimestampField
+
+intention_base_fields = {
+    "id": fields.String,
+    "name": fields.String,
+}
+
+intention_fields = {
+    "id": fields.String,
+    "name": fields.String,
+    "type_id": fields.String,
+    "type_name": fields.String,
+}
+
+intention_type_fields = {
+    "id": fields.String,
+    "name": fields.String,
+}
+
+intention_type_page_fields = {
+    "id": fields.String,
+    "name": fields.String,
+    "intention_count": fields.Integer,
+}
+
+intention_type_detail_fields = {
+    "id": fields.String,
+    "name": fields.String,
+    "intentions": fields.List(fields.Nested(intention_base_fields)),
+}
+
+intention_keyword_fields = {
+    "id": fields.String,
+    "name": fields.String,
+    "intention_id": fields.String,
+}
+
+intention_keyword_detail_fields = {
+    "id": fields.String,
+    "name": fields.String,
+    "intention": fields.Nested(intention_fields)
+}
+
+intention_corpus_fields = {
+    "id": fields.String,
+    "question": fields.String,
+    "question_config": fields.String,
+    "intention_id": fields.String,
+    "created_by": fields.String,
+    "created_at": TimestampField,
+    "updated_by": fields.String,
+    "updated_at": TimestampField,
+}
+
+intention_corpus_similarity_question_fields = {
+    "id": fields.String,
+    "question": fields.String,
+    "question_config": fields.String,
+    "corpus_id": fields.String,
+}
+
+intention_page_fields = {
+    "id": fields.String,
+    "name": fields.String,
+    "type_id": fields.String,
+    "type_name": fields.String,
+    "corpus_count": fields.Integer,
+    "keywords_count": fields.Integer,
+    "created_by": fields.String,
+    "created_at": TimestampField,
+}
+
+intention_corpus_detail_fields = {
+    "id": fields.String,
+    "question": fields.String,
+    "question_config": fields.String,
+    "intention": fields.Nested(intention_page_fields),
+    "similarity_questions": fields.List(fields.Nested(intention_corpus_similarity_question_fields)),
+}
+
+intention_detail_fields = {
+    "id": fields.String,
+    "name": fields.String,
+    "type": fields.Nested(intention_type_fields),
+    "corpus": fields.List(fields.Nested(intention_corpus_fields)),
+    "keywords": fields.List(fields.Nested(intention_keyword_fields)),
+    "created_by": fields.String,
+    "created_at": TimestampField,
+    "updated_by": fields.String,
+    "updated_at": TimestampField,
+}
+
+

+ 176 - 0
api/models/intention.py

@@ -0,0 +1,176 @@
+from sqlalchemy import func
+
+from .engine import db
+from .types import StringUUID
+
+
+class IntentionType(db.Model):
+    __tablename__ = 'intention_types'
+    __table_args__ = (
+        db.PrimaryKeyConstraint('id', name='intention_type_id_pkey'),
+    )
+
+    id = db.Column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()"))
+    name = db.Column(db.String(255), nullable=False)
+    created_by = db.Column(StringUUID, nullable=False)
+    created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp())
+    updated_by = db.Column(StringUUID, nullable=True)
+    updated_at = db.Column(db.DateTime, nullable=True, server_default=func.current_timestamp())
+
+    @property
+    def intention_count(self):
+        return (
+            db.session.query(func.count(Intention.id))
+            .filter(Intention.type_id==self.id)
+            .scalar()
+        )
+
+    @property
+    def intentions(self):
+        return (
+            db.session.query(Intention)
+            .filter(Intention.type_id == self.id)
+            .all()
+        )
+
+class Intention(db.Model):
+    __tablename__ = "intentions"
+    __table_args__ = (
+        db.PrimaryKeyConstraint("id", name="intention_pkey"),
+        db.Index("intention_type_idx", "type_id"),
+    )
+
+    id = db.Column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()"))
+    type_id = db.Column(StringUUID, nullable=False)
+    name = db.Column(db.String(255), nullable=False)
+    created_by = db.Column(StringUUID, nullable=False)
+    created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp())
+    updated_by = db.Column(StringUUID, nullable=True)
+    updated_at = db.Column(db.DateTime, nullable=True, server_default=func.current_timestamp())
+
+    @property
+    def type_name(self):
+        return (
+            db.session.query(IntentionType.name)
+            .filter(IntentionType.id==self.type_id)
+            .first().name
+        )
+
+    @property
+    def type(self):
+        return (
+            db.session.query(IntentionType)
+            .filter(IntentionType.id==self.type_id)
+            .first()
+        )
+
+    @property
+    def corpus(self):
+        return (
+            db.session.query(IntentionCorpus)
+            .filter(IntentionCorpus.intention_id==self.id)
+            .all()
+        )
+
+    @property
+    def keywords(self):
+        return (
+            db.session.query(IntentionKeyword)
+            .filter(IntentionKeyword.intention_id==self.id)
+            .all()
+        )
+
+    @property
+    def corpus_count(self):
+        return (
+            db.session.query(func.count(IntentionCorpus.id))
+            .filter(IntentionCorpus.intention_id == self.id)
+            .scalar()
+        )
+
+    @property
+    def keywords_count(self):
+        return (
+            db.session.query(func.count(IntentionKeyword.id))
+            .filter(IntentionKeyword.intention_id == self.id)
+            .scalar()
+        )
+
+class IntentionKeyword(db.Model):
+    __tablename__ = "intention_keywords"
+    __table_args__ = (
+        db.PrimaryKeyConstraint('id', name='intention_keyword_pkey'),
+    )
+
+    id = db.Column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()"))
+    name = db.Column(db.String(255), nullable=False)
+    intention_id = db.Column(StringUUID, nullable=False)
+    created_by = db.Column(StringUUID, nullable=False)
+    created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp())
+    updated_by = db.Column(StringUUID, nullable=True)
+    updated_at = db.Column(db.DateTime, nullable=True, server_default=func.current_timestamp())
+
+    @property
+    def intention(self):
+        return (
+            db.session.query(Intention).filter(Intention.id==self.intention_id).first()
+        )
+
+class IntentionCorpus(db.Model):
+    __tablename__ = "intention_corpus"
+    __table_args__ = (
+        db.PrimaryKeyConstraint('id', name='intention_corpus_pkey'),
+        db.Index("intention_corpus_idx", "intention_id"),
+    )
+
+    id = db.Column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()"))
+    question = db.Column(db.String(255), nullable=False)
+    question_config = db.Column(db.JSON, nullable=False)
+    intention_id = db.Column(StringUUID, nullable=False)
+    created_by = db.Column(StringUUID, nullable=False)
+    created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp())
+    updated_by = db.Column(StringUUID, nullable=True)
+    updated_at = db.Column(db.DateTime, nullable=True, server_default=func.current_timestamp())
+
+    @property
+    def intention(self):
+        return (
+            db.session.query(Intention)
+            .filter(Intention.id==self.intention_id)
+            .first()
+        )
+
+    @property
+    def similarity_questions(self):
+        return (
+            db.session.query(IntentionCorpusSimilarityQuestion)
+            .filter(IntentionCorpusSimilarityQuestion.corpus_id==self.id)
+            .all()
+        )
+
+class IntentionCorpusSimilarityQuestion(db.Model):
+    __tablename__ = "intention_corpus_similarity_questions"
+    __table_args__ = (
+        db.PrimaryKeyConstraint('id', name='intention_corpus_similarity_question_pkey'),
+        db.Index("intention_corpus_similarity_question_idx", "corpus_id"),
+    )
+
+    id = db.Column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()"))
+    question = db.Column(db.String(255), nullable=False)
+    question_config = db.Column(db.JSON, nullable=False)
+    corpus_id = db.Column(StringUUID, nullable=False)
+    created_by = db.Column(StringUUID, nullable=False)
+    created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp())
+    updated_by = db.Column(StringUUID, nullable=True)
+    updated_at = db.Column(db.DateTime, nullable=True, server_default=func.current_timestamp())
+
+    @property
+    def corpus(self):
+        return (
+            db.session.query(IntentionCorpus)
+            .filter(IntentionCorpus.id==self.corpus_id)
+            .first()
+        )
+
+
+

+ 17 - 0
api/services/errors/intention.py

@@ -0,0 +1,17 @@
+from services.errors.base import BaseServiceError
+
+
+class IntentionNameDuplicateError(BaseServiceError):
+    pass
+
+class IntentionTypeNameDuplicateError(BaseServiceError):
+    pass
+
+class IntentionKeywordNameDuplicateError(BaseServiceError):
+    pass
+
+class IntentionCorpusQuestionDuplicateError(BaseServiceError):
+    pass
+
+class IntentionCorpusSimilarityQuestionDuplicateError(BaseServiceError):
+    pass

+ 4 - 4
api/services/external_application_service.py

@@ -16,16 +16,16 @@ class ExternalApplicationService:
         query = ExternalApplication.query.order_by(ExternalApplication.created_at.desc())
 
         if search:
-            query = ExternalApplication.query.filter(ExternalApplication.name.ilike(f"%{search}%"))
+            query = query.filter(ExternalApplication.name.ilike(f"%{search}%"))
 
         if type:
-            query = ExternalApplication.query.filter(ExternalApplication.type == type)
+            query = query.filter(ExternalApplication.type == type)
 
         if url:
-            query = ExternalApplication.query.filter(ExternalApplication.url == url)
+            query = query.filter(ExternalApplication.url == url)
 
         if method:
-            query = ExternalApplication.query.filter(ExternalApplication.method == method)
+            query = query.filter(ExternalApplication.method == method)
 
         external_applications = query.paginate(page=page, per_page=per_page, max_per_page=100, error_out=False)
         return external_applications.items, external_applications.total

+ 502 - 0
api/services/intention_service.py

@@ -0,0 +1,502 @@
+import logging
+import uuid
+from datetime import datetime
+from typing import Optional
+
+from flask_login import current_user
+from werkzeug.exceptions import Forbidden, NotFound
+
+from extensions.ext_database import db
+from models.intention import (
+    Intention,
+    IntentionCorpus,
+    IntentionCorpusSimilarityQuestion,
+    IntentionKeyword,
+    IntentionType,
+)
+from services.errors.intention import (
+    IntentionCorpusQuestionDuplicateError,
+    IntentionKeywordNameDuplicateError,
+    IntentionNameDuplicateError,
+    IntentionTypeNameDuplicateError,
+)
+
+
+class IntentionTypeService:
+    @staticmethod
+    def get_intention_types(page, per_page, search=None):
+        query = IntentionType.query.order_by(IntentionType.created_at.desc())
+        if search:
+            query = query.filter(IntentionType.name.like(f"%{search}%"))
+        intention_types = query.paginate(page=page, per_page=per_page, error_out=False)
+        return intention_types.items, intention_types.total
+
+    @staticmethod
+    def save_intention_type(args: dict) -> IntentionType:
+        name = args['name']
+        intention_type = IntentionTypeService.get_intention_type_by_name(name)
+        if intention_type:
+            raise IntentionTypeNameDuplicateError(f"IntentionType with name {name} already exists.")
+        intention_type = IntentionType(
+            id=str(uuid.uuid4()),
+            name=name,
+            created_by=current_user.id,
+            created_at=datetime.now(),
+        )
+        db.session.add(intention_type)
+        db.session.commit()
+        return intention_type
+
+    @staticmethod
+    def update_intention_type(id: str, args: dict) -> IntentionType:
+        intention_type = IntentionTypeService.get_intention_type(id)
+        if not intention_type:
+            raise NotFound("IntentionType not found")
+
+        name = args['name']
+        intention_type_new = (
+            IntentionType.query.filter(
+                IntentionType.id != intention_type.id,
+                IntentionType.name == name,
+            ).first()
+        )
+        if intention_type_new:
+            raise IntentionTypeNameDuplicateError(f"IntentionType with name {name} already exists.")
+        intention_type.name = name
+
+        intention_type.updated_by = current_user.id
+        intention_type.updated_at = datetime.now()
+        db.session.add(intention_type)
+        db.session.commit()
+        return intention_type
+
+    @staticmethod
+    def delete_intention_type(id: str):
+        intention_type = IntentionTypeService.get_intention_type(id)
+        if not intention_type:
+            raise NotFound("IntentionType not found")
+        db.session.delete(intention_type)
+        db.session.commit()
+        return intention_type
+
+    @staticmethod
+    def get_intention_type(id: str) -> Optional[IntentionType]:
+        intention_type: Optional[IntentionType] = IntentionType.query.filter_by(id=id).first()
+        return intention_type
+
+    @staticmethod
+    def get_intention_type_by_name(name: str) -> Optional[IntentionType]:
+        intention_type: Optional[IntentionType] = IntentionType.query.filter_by(name=name).first()
+        return intention_type
+
+class IntentionService:
+    @staticmethod
+    def get_intentions(page, per_page, type_id=None, name_search=None):
+        query = Intention.query.order_by(Intention.created_at.desc())
+
+        if type_id:
+            query = query.filter(Intention.type_id == type_id)
+
+        if name_search:
+            query = query.filter(Intention.name.ilike(f"%{name_search}%"))
+
+        intentions = query.paginate(page=page, per_page=per_page, error_out=False)
+        return intentions.items, intentions.total
+
+    @staticmethod
+    def save_intention(args: dict) -> Intention:
+        name = args["name"]
+        intention = IntentionService.get_intention_by_name(name)
+        if intention:
+            raise IntentionNameDuplicateError(f"Intention with name {name} already exists.")
+
+        type_id = args["type_id"]
+        intention_type = IntentionTypeService.get_intention_type(type_id)
+        if not intention_type:
+            raise NotFound("IntentionType not found")
+
+        intention = Intention(
+            id=str(uuid.uuid4()),
+            name=name,
+            type_id=type_id,
+            created_by=current_user.id,
+            created_at=datetime.now(),
+        )
+        db.session.add(intention)
+        db.session.commit()
+        return intention
+
+    @staticmethod
+    def update_intention(intention_id: str, args: dict) -> Intention:
+        intention: Optional[Intention] = IntentionService.get_intention(intention_id)
+        if not intention:
+            raise NotFound("Intention not found")
+
+        name = args["name"]
+        intention_new = (
+            Intention.query.filter(
+                Intention.id != intention.id,
+                Intention.name == name
+            ).first()
+        )
+        if intention_new:
+            raise IntentionNameDuplicateError(f"Intention with name {name} already exists.")
+
+        intention.name = name
+
+        type_id = args["type_id"]
+        intention_type = IntentionTypeService.get_intention_type(type_id)
+        if not intention_type:
+            raise NotFound("IntentionType not found")
+        intention.type_id = type_id
+        intention.updated_by = current_user.id
+        intention.updated_at = datetime.now()
+        db.session.add(intention)
+        db.session.commit()
+        return intention
+
+    @staticmethod
+    def delete_intention(intention_id: str):
+        intention: Optional[Intention] = IntentionService.get_intention(intention_id)
+        if not intention:
+            raise NotFound("Intention not found")
+
+        # 1.若存在关键词,则无法删除
+        if intention.keywords_count > 0:
+            raise Forbidden(f"You are not allowed to delete intention, "
+                            f"because {intention.keywords_count} keywords were found.")
+
+        # 2.若关联训练预料,则无法删除
+        if intention.corpus_count > 0:
+            raise Forbidden(f"You are not allowed to delete intention, "
+                            f"because {intention.corpus_count} corpus were found.")
+
+        db.session.delete(intention)
+        db.session.commit()
+        return intention
+
+    @staticmethod
+    def get_intention(intention_id: str) -> Optional[Intention]:
+        intention: Optional[Intention] = Intention.query.filter(Intention.id == intention_id).first()
+        return intention
+
+    @staticmethod
+    def get_intention_by_name(name: str) -> Optional[Intention]:
+        intention: Optional[Intention] = Intention.query.filter(Intention.name == name).first()
+        return intention
+
+class IntentionKeywordService:
+    @staticmethod
+    def get_intention_keywords(intention_id: str, search=None):
+        query = IntentionKeyword.query.filter_by(intention_id=intention_id)
+        if search:
+            query = query.filter(IntentionKeyword.name.ilike(f"%{search}%"))
+        intention_keywords = query.all()
+        return intention_keywords
+
+    @staticmethod
+    def save_intention_keyword(intention_id: str, args: dict) -> IntentionKeyword:
+        name = args["name"]
+        intention_keyword = IntentionKeywordService.get_intention_keyword_by_name(intention_id, name)
+        if intention_keyword:
+            raise IntentionKeywordNameDuplicateError(f"IntentionKeyword with name {name} already exists.")
+        intention_keyword = IntentionKeyword(
+            id=str(uuid.uuid4()),
+            intention_id=intention_id,
+            name=name,
+            created_by=current_user.id,
+            created_at=datetime.now(),
+        )
+        db.session.add(intention_keyword)
+        db.session.commit()
+        return intention_keyword
+
+    @staticmethod
+    def update_intention_keyword(intention_keyword_id: str, args: dict) -> IntentionKeyword:
+        intention_keyword = IntentionKeywordService.get_intention_keyword(intention_keyword_id)
+        if not intention_keyword:
+            raise NotFound("IntentionKeyword not found")
+
+        name = args["name"]
+        intention_keyword_new = (
+            IntentionKeyword.query.filter(
+                IntentionKeyword.id != intention_keyword.id,
+                IntentionKeyword.name == name)
+            .first()
+        )
+        if intention_keyword_new:
+            raise IntentionKeywordNameDuplicateError(f"IntentionKeyword with name {name} already exists.")
+        intention_keyword.name = name
+
+        intention_id = args["intention_id"]
+        intention = IntentionService.get_intention(intention_id)
+        if not intention:
+            raise NotFound("Intention not found")
+
+        intention_keyword.intention_id = intention_id
+        intention_keyword.updated_by = current_user.id
+        intention_keyword.updated_at = datetime.now()
+        db.session.add(intention_keyword)
+        db.session.commit()
+        return intention_keyword
+
+    @staticmethod
+    def delete_intention_keyword(intention_keyword_id: str):
+        intention_keyword = IntentionKeywordService.get_intention_keyword(intention_keyword_id)
+        if not intention_keyword:
+            raise NotFound("IntentionKeyword not found")
+        db.session.delete(intention_keyword)
+        db.session.commit()
+
+    @staticmethod
+    def delete_intention_keywords(intention_keyword_ids: list[str]):
+        intention_keywords = IntentionKeyword.query.filter(IntentionKeyword.id.in_(intention_keyword_ids)).all()
+        for intention_keyword in intention_keywords:
+            db.session.delete(intention_keyword)
+        db.session.commit()
+
+    @staticmethod
+    def delete_intention_keywords_by_intention_id(intention_id: str):
+        intention_keywords = IntentionKeyword.query.filter(IntentionKeyword.intention_id == intention_id).all()
+        for intention_keyword in intention_keywords:
+            db.session.delete(intention_keyword)
+        db.session.commit()
+
+    @staticmethod
+    def get_intention_keyword(intention_keyword_id: str) -> Optional[IntentionKeyword]:
+        intention_keyword: Optional[IntentionKeyword] = (
+            IntentionKeyword.query.filter_by(id=intention_keyword_id).first()
+        )
+        return intention_keyword
+
+    @staticmethod
+    def get_intention_keyword_by_name(intention_id, name: str) -> Optional[IntentionKeyword]:
+        intention_keyword: Optional[IntentionKeyword] = (
+            IntentionKeyword.query.filter(
+                IntentionKeyword.intention_id == intention_id,
+                IntentionKeyword.name == name,
+            ).first()
+        )
+        return intention_keyword
+
+class IntentionCorpusService:
+    @staticmethod
+    def get_intention_corpus(corpus_id: str) -> Optional[IntentionCorpus]:
+        intention_corpus: Optional[IntentionCorpus] = (
+            IntentionCorpus.query.filter(IntentionCorpus.id == corpus_id).first()
+        )
+        return intention_corpus
+
+    @staticmethod
+    def get_page_intention_corpus(page, per_page, search=None, intention_id=None):
+        query = IntentionCorpus.query.order_by(IntentionCorpus.created_at.desc())
+        if search:
+            query = query.filter(IntentionCorpus.question.like(f"%{search}%"))
+
+        if intention_id:
+            query = query.filter(IntentionCorpus.intention_id == intention_id)
+
+        intention_corpus = query.paginate(page=page, per_page=per_page, error_out=False)
+        return intention_corpus.items, intention_corpus.total
+
+    @staticmethod
+    def get_intention_corpus_by_question(question: str) -> Optional[IntentionCorpus]:
+        intention_corpus: Optional[IntentionCorpus] = (
+            IntentionCorpus.query.filter(IntentionCorpus.question == question).first()
+        )
+        return intention_corpus
+
+    @staticmethod
+    def save_intention_corpus(args: dict):
+        question = args["question"]
+        intention_corpus = IntentionCorpusService.get_intention_corpus_by_question(question)
+        if intention_corpus:
+            raise IntentionCorpusQuestionDuplicateError(f"IntentionCorpus with question {question} already exists.")
+
+        intention_id = args["intention_id"]
+        intention = IntentionService.get_intention(intention_id)
+        if not intention:
+            raise NotFound(f"Intention with id {intention_id} not found")
+
+        intention_corpus = IntentionCorpus(
+            id=str(uuid.uuid4()),
+            question=question,
+            intention_id=intention_id,
+            created_by=current_user.id,
+            created_at=datetime.now(),
+        )
+
+        if "question_config" in args:
+            intention_corpus.question_config = args["question_config"]
+
+        db.session.add(intention_corpus)
+        db.session.commit()
+        return intention_corpus
+
+    @staticmethod
+    def update_intention_corpus(corpus_id: str, args: dict):
+        intention_corpus = IntentionCorpusService.get_intention_corpus(corpus_id)
+        if not intention_corpus:
+            raise NotFound(f"IntentionCorpus with id {corpus_id} not found")
+
+        if "question" in args:
+            question = args["question"]
+            intention_corpus_new = (
+                IntentionCorpus.query.filter(
+                    IntentionCorpus.id != corpus_id,
+                    IntentionCorpus.question == question
+                ).first()
+            )
+            if intention_corpus_new:
+                raise IntentionCorpusQuestionDuplicateError(f"IntentionCorpus with question {question} already exists.")
+            intention_corpus.question = question
+
+        if "question_config" in args:
+            intention_corpus.question_config = args["question_config"]
+
+        if "intention_id" in args:
+            intention_id = args["intention_id"]
+            intention = IntentionService.get_intention(intention_id)
+            if not intention:
+                raise NotFound(f"Intention with id {intention_id} not found")
+            intention_corpus.intention_id = intention.id
+
+        intention_corpus.updated_at = datetime.now()
+        intention_corpus.updated_by = current_user.id
+        db.session.add(intention_corpus)
+        db.session.commit()
+        return intention_corpus
+
+    @staticmethod
+    def delete_intention_corpus_by_id(corpus_id: str):
+        intention_corpus = IntentionCorpusService.get_intention_corpus(corpus_id)
+        if not intention_corpus:
+            raise NotFound(f"IntentionCorpus with id {corpus_id} not found")
+        IntentionCorpusService.delete_intention_corpus(intention_corpus)
+
+    @staticmethod
+    def delete_intention_corpus(intention_corpus: IntentionCorpus):
+        similarity_questions = intention_corpus.similarity_questions
+        if similarity_questions:
+            raise Forbidden(f"存在与其关联的相似问题,无法删除Id为{intention_corpus.id}训练语料")
+
+        db.session.delete(intention_corpus)
+        db.session.commit()
+
+class IntentionCorpusSimilarityQuestionService:
+    @staticmethod
+    def save_similarity_question(corpus_id: str, args: dict):
+        intention_corpus = IntentionCorpusService.get_intention_corpus(corpus_id)
+        if not intention_corpus:
+            raise NotFound(f"IntentionCorpus with id {corpus_id} not found")
+
+        question = args["question"]
+        intention_corpus_similarity_question = (
+            IntentionCorpusSimilarityQuestionService.get_similarity_question_by_question(question)
+        )
+        if intention_corpus_similarity_question:
+            raise IntentionCorpusQuestionDuplicateError(f"IntentionCorpus with question {question} already exists.")
+
+        intention_corpus_similarity_question = IntentionCorpusSimilarityQuestion(
+            id=str(uuid.uuid4()),
+            question=question,
+            corpus_id=corpus_id,
+            created_by=current_user.id,
+            created_at=datetime.now(),
+        )
+
+        if "question_config" in args:
+            intention_corpus_similarity_question.question_config = args["question_config"]
+        db.session.add(intention_corpus_similarity_question)
+        db.session.commit()
+        return intention_corpus_similarity_question
+
+    @staticmethod
+    def update_similarity_question(similarity_question_id: str, args: dict):
+        similarity_question = IntentionCorpusSimilarityQuestionService.get_similarity_question(similarity_question_id)
+        if not similarity_question:
+            raise NotFound(f"IntentionCorpus with id {similarity_question_id} not found")
+
+        if "corpus_id" in args:
+            corpus_id = args["corpus_id"]
+            intention_corpus = IntentionCorpusService.get_intention_corpus(corpus_id)
+            if not intention_corpus:
+                raise NotFound(f"IntentionCorpus with id {corpus_id} not found")
+            similarity_question.corpus_id = corpus_id
+
+        if "question" in args:
+            similarity_question.question = args["question"]
+
+        if "question_config" in args:
+            similarity_question.question_config = args["question_config"]
+
+        db.session.add(similarity_question)
+        db.session.commit()
+        return similarity_question
+
+    @staticmethod
+    def get_similarity_question(similarity_question_id: str) -> Optional[IntentionCorpusSimilarityQuestion]:
+        similarity_question: Optional[IntentionCorpus] = (
+            IntentionCorpusSimilarityQuestion.query.filter_by(id=similarity_question_id).first()
+        )
+        return similarity_question
+
+    @staticmethod
+    def get_similarity_question_by_question(question: str) -> Optional[IntentionCorpusSimilarityQuestion]:
+        similarity_question: Optional[IntentionCorpusSimilarityQuestion] = (
+            IntentionCorpusSimilarityQuestion.query.filter_by(question=question).first()
+        )
+        return similarity_question
+
+    @staticmethod
+    def get_similarity_questions_by_corpus_id_like_question(corpus_id, search = None):
+        query = (
+            IntentionCorpusSimilarityQuestion.query
+            .filter(IntentionCorpusSimilarityQuestion.corpus_id==corpus_id)
+            .order_by(IntentionCorpusSimilarityQuestion.created_at.desc())
+        )
+        if search:
+            query = query.filter(IntentionCorpusSimilarityQuestion.question.ilike(f"%{search}%"))
+
+        similarity_questions = query.all()
+        return similarity_questions
+
+    @staticmethod
+    def delete_similarity_question_by_corpus_id(corpus_id: str):
+        intention_corpus = IntentionCorpusService.get_intention_corpus(corpus_id)
+        if not intention_corpus:
+            raise NotFound(f"IntentionCorpus with id {corpus_id} not found")
+        logging.info(intention_corpus.similarity_questions)
+        IntentionCorpusSimilarityQuestionService.delete_similarity_questions(intention_corpus.similarity_questions)
+
+    @staticmethod
+    def delete_similarity_question_by_id(similarity_question_id: str):
+        similarity_question = IntentionCorpusSimilarityQuestionService.get_similarity_question(similarity_question_id)
+        if not similarity_question:
+            raise NotFound(f"IntentionCorpus with id {similarity_question_id} not found")
+
+        IntentionCorpusSimilarityQuestionService.delete_similarity_question(similarity_question)
+
+    @staticmethod
+    def delete_similarity_question(similarity_question: IntentionCorpusSimilarityQuestion):
+        db.session.delete(similarity_question)
+        db.session.commit()
+
+    @staticmethod
+    def delete_similarity_questions_by_ids(similarity_question_ids: list[str]):
+        similarity_questions = (
+            IntentionCorpusSimilarityQuestion.query
+            .filter(IntentionCorpusSimilarityQuestion.id.in_(similarity_question_ids))
+            .all()
+        )
+        IntentionCorpusSimilarityQuestionService.delete_similarity_questions(similarity_questions)
+
+
+    @staticmethod
+    def delete_similarity_questions(similarity_questions: list[IntentionCorpusSimilarityQuestion]):
+        if not similarity_questions:
+            return
+
+        for similarity_question in similarity_questions:
+            db.session.delete(similarity_question)
+        db.session.commit()