From 9b1afb68ebb3fdd224a37e451ea4cef8e17d8669 Mon Sep 17 00:00:00 2001 From: takatost Date: Wed, 28 Feb 2024 22:16:24 +0800 Subject: [PATCH] add features update api refactor app model config validation --- api/controllers/console/app/model_config.py | 43 +- api/core/apps/__init__.py | 0 .../apps/app_config_validators/__init__.py | 0 .../advanced_chat_app.py | 54 ++ .../app_config_validators/agent_chat_app.py | 82 +++ .../apps/app_config_validators/chat_app.py | 82 +++ .../app_config_validators/completion_app.py | 67 +++ .../app_config_validators/workflow_app.py | 34 ++ api/core/apps/config_validators/__init__.py | 0 api/core/apps/config_validators/agent.py | 82 +++ api/core/apps/config_validators/dataset.py | 141 +++++ .../config_validators/external_data_tools.py | 40 ++ .../apps/config_validators/file_upload.py | 38 ++ api/core/apps/config_validators/model.py | 83 +++ api/core/apps/config_validators/moderation.py | 36 ++ .../apps/config_validators/more_like_this.py | 26 + .../config_validators/opening_statement.py | 29 + api/core/apps/config_validators/prompt.py | 87 +++ .../config_validators/retriever_resource.py | 26 + .../apps/config_validators/speech_to_text.py | 26 + .../config_validators/suggested_questions.py | 26 + .../apps/config_validators/text_to_speech.py | 30 + .../apps/config_validators/user_input_form.py | 62 ++ api/services/app_model_config_service.py | 539 +----------------- api/services/completion_service.py | 11 +- api/services/workflow_service.py | 2 +- 26 files changed, 1115 insertions(+), 531 deletions(-) create mode 100644 api/core/apps/__init__.py create mode 100644 api/core/apps/app_config_validators/__init__.py create mode 100644 api/core/apps/app_config_validators/advanced_chat_app.py create mode 100644 api/core/apps/app_config_validators/agent_chat_app.py create mode 100644 api/core/apps/app_config_validators/chat_app.py create mode 100644 api/core/apps/app_config_validators/completion_app.py create mode 100644 api/core/apps/app_config_validators/workflow_app.py create mode 100644 api/core/apps/config_validators/__init__.py create mode 100644 api/core/apps/config_validators/agent.py create mode 100644 api/core/apps/config_validators/dataset.py create mode 100644 api/core/apps/config_validators/external_data_tools.py create mode 100644 api/core/apps/config_validators/file_upload.py create mode 100644 api/core/apps/config_validators/model.py create mode 100644 api/core/apps/config_validators/moderation.py create mode 100644 api/core/apps/config_validators/more_like_this.py create mode 100644 api/core/apps/config_validators/opening_statement.py create mode 100644 api/core/apps/config_validators/prompt.py create mode 100644 api/core/apps/config_validators/retriever_resource.py create mode 100644 api/core/apps/config_validators/speech_to_text.py create mode 100644 api/core/apps/config_validators/suggested_questions.py create mode 100644 api/core/apps/config_validators/text_to_speech.py create mode 100644 api/core/apps/config_validators/user_input_form.py diff --git a/api/controllers/console/app/model_config.py b/api/controllers/console/app/model_config.py index 912c4eab9a..050c688c28 100644 --- a/api/controllers/console/app/model_config.py +++ b/api/controllers/console/app/model_config.py @@ -2,7 +2,7 @@ import json from flask import request from flask_login import current_user -from flask_restful import Resource +from flask_restful import Resource, reqparse from controllers.console import api from controllers.console.app.wraps import get_app_model @@ -14,7 +14,7 @@ from core.tools.utils.configuration import ToolParameterConfigurationManager from events.app_event import app_model_config_was_updated from extensions.ext_database import db from libs.login import login_required -from models.model import AppModelConfig +from models.model import AppModelConfig, AppMode from services.app_model_config_service import AppModelConfigService @@ -23,15 +23,14 @@ class ModelConfigResource(Resource): @setup_required @login_required @account_initialization_required - @get_app_model + @get_app_model(mode=[AppMode.AGENT_CHAT, AppMode.CHAT, AppMode.COMPLETION]) def post(self, app_model): """Modify app model config""" # validate config model_configuration = AppModelConfigService.validate_configuration( tenant_id=current_user.current_tenant_id, - account=current_user, config=request.json, - app_mode=app_model.mode + app_mode=AppMode.value_of(app_model.mode) ) new_app_model_config = AppModelConfig( @@ -129,4 +128,38 @@ class ModelConfigResource(Resource): return {'result': 'success'} +class FeaturesResource(Resource): + + @setup_required + @login_required + @account_initialization_required + @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) + def put(self, app_model): + """Get app features""" + parser = reqparse.RequestParser() + parser.add_argument('features', type=dict, required=True, nullable=False, location='json') + args = parser.parse_args() + + model_configuration = AppModelConfigService.validate_features( + tenant_id=current_user.current_tenant_id, + config=args.get('features'), + app_mode=AppMode.value_of(app_model.mode) + ) + + # update config + app_model_config = app_model.app_model_config + app_model_config.from_model_config_dict(model_configuration) + db.session.commit() + + app_model_config_was_updated.send( + app_model, + app_model_config=app_model_config + ) + + return { + 'result': 'success' + } + + api.add_resource(ModelConfigResource, '/apps//model-config') +api.add_resource(FeaturesResource, '/apps//features') diff --git a/api/core/apps/__init__.py b/api/core/apps/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/apps/app_config_validators/__init__.py b/api/core/apps/app_config_validators/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/apps/app_config_validators/advanced_chat_app.py b/api/core/apps/app_config_validators/advanced_chat_app.py new file mode 100644 index 0000000000..dc7664b844 --- /dev/null +++ b/api/core/apps/app_config_validators/advanced_chat_app.py @@ -0,0 +1,54 @@ +from core.apps.config_validators.file_upload import FileUploadValidator +from core.apps.config_validators.moderation import ModerationValidator +from core.apps.config_validators.opening_statement import OpeningStatementValidator +from core.apps.config_validators.retriever_resource import RetrieverResourceValidator +from core.apps.config_validators.speech_to_text import SpeechToTextValidator +from core.apps.config_validators.suggested_questions import SuggestedQuestionsValidator +from core.apps.config_validators.text_to_speech import TextToSpeechValidator + + +class AdvancedChatAppConfigValidator: + @classmethod + def config_validate(cls, tenant_id: str, config: dict) -> dict: + """ + Validate for advanced chat app model config + + :param tenant_id: tenant id + :param config: app model config args + """ + related_config_keys = [] + + # file upload validation + config, current_related_config_keys = FileUploadValidator.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # opening_statement + config, current_related_config_keys = OpeningStatementValidator.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # suggested_questions_after_answer + config, current_related_config_keys = SuggestedQuestionsValidator.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # speech_to_text + config, current_related_config_keys = SpeechToTextValidator.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # text_to_speech + config, current_related_config_keys = TextToSpeechValidator.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # return retriever resource + config, current_related_config_keys = RetrieverResourceValidator.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # moderation validation + config, current_related_config_keys = ModerationValidator.validate_and_set_defaults(tenant_id, config) + related_config_keys.extend(current_related_config_keys) + + related_config_keys = list(set(related_config_keys)) + + # Filter out extra parameters + filtered_config = {key: config.get(key) for key in related_config_keys} + + return filtered_config diff --git a/api/core/apps/app_config_validators/agent_chat_app.py b/api/core/apps/app_config_validators/agent_chat_app.py new file mode 100644 index 0000000000..d507fae685 --- /dev/null +++ b/api/core/apps/app_config_validators/agent_chat_app.py @@ -0,0 +1,82 @@ +from core.apps.config_validators.agent import AgentValidator +from core.apps.config_validators.external_data_tools import ExternalDataToolsValidator +from core.apps.config_validators.file_upload import FileUploadValidator +from core.apps.config_validators.model import ModelValidator +from core.apps.config_validators.moderation import ModerationValidator +from core.apps.config_validators.opening_statement import OpeningStatementValidator +from core.apps.config_validators.prompt import PromptValidator +from core.apps.config_validators.retriever_resource import RetrieverResourceValidator +from core.apps.config_validators.speech_to_text import SpeechToTextValidator +from core.apps.config_validators.suggested_questions import SuggestedQuestionsValidator +from core.apps.config_validators.text_to_speech import TextToSpeechValidator +from core.apps.config_validators.user_input_form import UserInputFormValidator +from models.model import AppMode + + +class AgentChatAppConfigValidator: + @classmethod + def config_validate(cls, tenant_id: str, config: dict) -> dict: + """ + Validate for agent chat app model config + + :param tenant_id: tenant id + :param config: app model config args + """ + app_mode = AppMode.AGENT_CHAT + + related_config_keys = [] + + # model + config, current_related_config_keys = ModelValidator.validate_and_set_defaults(tenant_id, config) + related_config_keys.extend(current_related_config_keys) + + # user_input_form + config, current_related_config_keys = UserInputFormValidator.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # external data tools validation + config, current_related_config_keys = ExternalDataToolsValidator.validate_and_set_defaults(tenant_id, config) + related_config_keys.extend(current_related_config_keys) + + # file upload validation + config, current_related_config_keys = FileUploadValidator.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # prompt + config, current_related_config_keys = PromptValidator.validate_and_set_defaults(app_mode, config) + related_config_keys.extend(current_related_config_keys) + + # agent_mode + config, current_related_config_keys = AgentValidator.validate_and_set_defaults(tenant_id, config) + related_config_keys.extend(current_related_config_keys) + + # opening_statement + config, current_related_config_keys = OpeningStatementValidator.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # suggested_questions_after_answer + config, current_related_config_keys = SuggestedQuestionsValidator.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # speech_to_text + config, current_related_config_keys = SpeechToTextValidator.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # text_to_speech + config, current_related_config_keys = TextToSpeechValidator.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # return retriever resource + config, current_related_config_keys = RetrieverResourceValidator.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # moderation validation + config, current_related_config_keys = ModerationValidator.validate_and_set_defaults(tenant_id, config) + related_config_keys.extend(current_related_config_keys) + + related_config_keys = list(set(related_config_keys)) + + # Filter out extra parameters + filtered_config = {key: config.get(key) for key in related_config_keys} + + return filtered_config diff --git a/api/core/apps/app_config_validators/chat_app.py b/api/core/apps/app_config_validators/chat_app.py new file mode 100644 index 0000000000..83c792e610 --- /dev/null +++ b/api/core/apps/app_config_validators/chat_app.py @@ -0,0 +1,82 @@ +from core.apps.config_validators.dataset import DatasetValidator +from core.apps.config_validators.external_data_tools import ExternalDataToolsValidator +from core.apps.config_validators.file_upload import FileUploadValidator +from core.apps.config_validators.model import ModelValidator +from core.apps.config_validators.moderation import ModerationValidator +from core.apps.config_validators.opening_statement import OpeningStatementValidator +from core.apps.config_validators.prompt import PromptValidator +from core.apps.config_validators.retriever_resource import RetrieverResourceValidator +from core.apps.config_validators.speech_to_text import SpeechToTextValidator +from core.apps.config_validators.suggested_questions import SuggestedQuestionsValidator +from core.apps.config_validators.text_to_speech import TextToSpeechValidator +from core.apps.config_validators.user_input_form import UserInputFormValidator +from models.model import AppMode + + +class ChatAppConfigValidator: + @classmethod + def config_validate(cls, tenant_id: str, config: dict) -> dict: + """ + Validate for chat app model config + + :param tenant_id: tenant id + :param config: app model config args + """ + app_mode = AppMode.CHAT + + related_config_keys = [] + + # model + config, current_related_config_keys = ModelValidator.validate_and_set_defaults(tenant_id, config) + related_config_keys.extend(current_related_config_keys) + + # user_input_form + config, current_related_config_keys = UserInputFormValidator.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # external data tools validation + config, current_related_config_keys = ExternalDataToolsValidator.validate_and_set_defaults(tenant_id, config) + related_config_keys.extend(current_related_config_keys) + + # file upload validation + config, current_related_config_keys = FileUploadValidator.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # prompt + config, current_related_config_keys = PromptValidator.validate_and_set_defaults(app_mode, config) + related_config_keys.extend(current_related_config_keys) + + # dataset_query_variable + config, current_related_config_keys = DatasetValidator.validate_and_set_defaults(tenant_id, app_mode, config) + related_config_keys.extend(current_related_config_keys) + + # opening_statement + config, current_related_config_keys = OpeningStatementValidator.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # suggested_questions_after_answer + config, current_related_config_keys = SuggestedQuestionsValidator.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # speech_to_text + config, current_related_config_keys = SpeechToTextValidator.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # text_to_speech + config, current_related_config_keys = TextToSpeechValidator.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # return retriever resource + config, current_related_config_keys = RetrieverResourceValidator.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # moderation validation + config, current_related_config_keys = ModerationValidator.validate_and_set_defaults(tenant_id, config) + related_config_keys.extend(current_related_config_keys) + + related_config_keys = list(set(related_config_keys)) + + # Filter out extra parameters + filtered_config = {key: config.get(key) for key in related_config_keys} + + return filtered_config diff --git a/api/core/apps/app_config_validators/completion_app.py b/api/core/apps/app_config_validators/completion_app.py new file mode 100644 index 0000000000..00371f8d05 --- /dev/null +++ b/api/core/apps/app_config_validators/completion_app.py @@ -0,0 +1,67 @@ +from core.apps.config_validators.dataset import DatasetValidator +from core.apps.config_validators.external_data_tools import ExternalDataToolsValidator +from core.apps.config_validators.file_upload import FileUploadValidator +from core.apps.config_validators.model import ModelValidator +from core.apps.config_validators.moderation import ModerationValidator +from core.apps.config_validators.more_like_this import MoreLikeThisValidator +from core.apps.config_validators.prompt import PromptValidator +from core.apps.config_validators.text_to_speech import TextToSpeechValidator +from core.apps.config_validators.user_input_form import UserInputFormValidator +from models.model import AppMode + + +class CompletionAppConfigValidator: + @classmethod + def config_validate(cls, tenant_id: str, config: dict) -> dict: + """ + Validate for completion app model config + + :param tenant_id: tenant id + :param config: app model config args + """ + app_mode = AppMode.COMPLETION + + related_config_keys = [] + + # model + config, current_related_config_keys = ModelValidator.validate_and_set_defaults(tenant_id, config) + related_config_keys.extend(current_related_config_keys) + + # user_input_form + config, current_related_config_keys = UserInputFormValidator.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # external data tools validation + config, current_related_config_keys = ExternalDataToolsValidator.validate_and_set_defaults(tenant_id, config) + related_config_keys.extend(current_related_config_keys) + + # file upload validation + config, current_related_config_keys = FileUploadValidator.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # prompt + config, current_related_config_keys = PromptValidator.validate_and_set_defaults(app_mode, config) + related_config_keys.extend(current_related_config_keys) + + # dataset_query_variable + config, current_related_config_keys = DatasetValidator.validate_and_set_defaults(tenant_id, app_mode, config) + related_config_keys.extend(current_related_config_keys) + + # text_to_speech + config, current_related_config_keys = TextToSpeechValidator.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # more_like_this + config, current_related_config_keys = MoreLikeThisValidator.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # moderation validation + config, current_related_config_keys = ModerationValidator.validate_and_set_defaults(tenant_id, config) + related_config_keys.extend(current_related_config_keys) + + related_config_keys = list(set(related_config_keys)) + + # Filter out extra parameters + filtered_config = {key: config.get(key) for key in related_config_keys} + + return filtered_config diff --git a/api/core/apps/app_config_validators/workflow_app.py b/api/core/apps/app_config_validators/workflow_app.py new file mode 100644 index 0000000000..545d3d79a3 --- /dev/null +++ b/api/core/apps/app_config_validators/workflow_app.py @@ -0,0 +1,34 @@ +from core.apps.config_validators.file_upload import FileUploadValidator +from core.apps.config_validators.moderation import ModerationValidator +from core.apps.config_validators.text_to_speech import TextToSpeechValidator + + +class WorkflowAppConfigValidator: + @classmethod + def config_validate(cls, tenant_id: str, config: dict) -> dict: + """ + Validate for workflow app model config + + :param tenant_id: tenant id + :param config: app model config args + """ + related_config_keys = [] + + # file upload validation + config, current_related_config_keys = FileUploadValidator.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # text_to_speech + config, current_related_config_keys = TextToSpeechValidator.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # moderation validation + config, current_related_config_keys = ModerationValidator.validate_and_set_defaults(tenant_id, config) + related_config_keys.extend(current_related_config_keys) + + related_config_keys = list(set(related_config_keys)) + + # Filter out extra parameters + filtered_config = {key: config.get(key) for key in related_config_keys} + + return filtered_config diff --git a/api/core/apps/config_validators/__init__.py b/api/core/apps/config_validators/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/apps/config_validators/agent.py b/api/core/apps/config_validators/agent.py new file mode 100644 index 0000000000..69f9338080 --- /dev/null +++ b/api/core/apps/config_validators/agent.py @@ -0,0 +1,82 @@ +import uuid +from typing import Tuple + +from core.agent.agent_executor import PlanningStrategy +from core.apps.config_validators.dataset import DatasetValidator + +OLD_TOOLS = ["dataset", "google_search", "web_reader", "wikipedia", "current_datetime"] + + +class AgentValidator: + @classmethod + def validate_and_set_defaults(cls, tenant_id: str, config: dict) -> Tuple[dict, list[str]]: + """ + Validate and set defaults for agent feature + + :param tenant_id: tenant ID + :param config: app model config args + """ + if not config.get("agent_mode"): + config["agent_mode"] = { + "enabled": False, + "tools": [] + } + + if not isinstance(config["agent_mode"], dict): + raise ValueError("agent_mode must be of object type") + + if "enabled" not in config["agent_mode"] or not config["agent_mode"]["enabled"]: + config["agent_mode"]["enabled"] = False + + if not isinstance(config["agent_mode"]["enabled"], bool): + raise ValueError("enabled in agent_mode must be of boolean type") + + if not config["agent_mode"].get("strategy"): + config["agent_mode"]["strategy"] = PlanningStrategy.ROUTER.value + + if config["agent_mode"]["strategy"] not in [member.value for member in list(PlanningStrategy.__members__.values())]: + raise ValueError("strategy in agent_mode must be in the specified strategy list") + + if not config["agent_mode"].get("tools"): + config["agent_mode"]["tools"] = [] + + if not isinstance(config["agent_mode"]["tools"], list): + raise ValueError("tools in agent_mode must be a list of objects") + + for tool in config["agent_mode"]["tools"]: + key = list(tool.keys())[0] + if key in OLD_TOOLS: + # old style, use tool name as key + tool_item = tool[key] + + if "enabled" not in tool_item or not tool_item["enabled"]: + tool_item["enabled"] = False + + if not isinstance(tool_item["enabled"], bool): + raise ValueError("enabled in agent_mode.tools must be of boolean type") + + if key == "dataset": + if 'id' not in tool_item: + raise ValueError("id is required in dataset") + + try: + uuid.UUID(tool_item["id"]) + except ValueError: + raise ValueError("id in dataset must be of UUID type") + + if not DatasetValidator.is_dataset_exists(tenant_id, tool_item["id"]): + raise ValueError("Dataset ID does not exist, please check your permission.") + else: + # latest style, use key-value pair + if "enabled" not in tool or not tool["enabled"]: + tool["enabled"] = False + if "provider_type" not in tool: + raise ValueError("provider_type is required in agent_mode.tools") + if "provider_id" not in tool: + raise ValueError("provider_id is required in agent_mode.tools") + if "tool_name" not in tool: + raise ValueError("tool_name is required in agent_mode.tools") + if "tool_parameters" not in tool: + raise ValueError("tool_parameters is required in agent_mode.tools") + + return config, ["agent_mode"] diff --git a/api/core/apps/config_validators/dataset.py b/api/core/apps/config_validators/dataset.py new file mode 100644 index 0000000000..32db038c21 --- /dev/null +++ b/api/core/apps/config_validators/dataset.py @@ -0,0 +1,141 @@ +import uuid +from typing import Tuple + +from core.agent.agent_executor import PlanningStrategy +from models.model import AppMode +from services.dataset_service import DatasetService + + +class DatasetValidator: + @classmethod + def validate_and_set_defaults(cls, tenant_id: str, app_mode: AppMode, config: dict) -> Tuple[dict, list[str]]: + """ + Validate and set defaults for dataset feature + + :param tenant_id: tenant ID + :param app_mode: app mode + :param config: app model config args + """ + # Extract dataset config for legacy compatibility + config = cls.extract_dataset_config_for_legacy_compatibility(tenant_id, app_mode, config) + + # dataset_configs + if not config.get("dataset_configs"): + config["dataset_configs"] = {'retrieval_model': 'single'} + + if not config["dataset_configs"].get("datasets"): + config["dataset_configs"]["datasets"] = { + "strategy": "router", + "datasets": [] + } + + if not isinstance(config["dataset_configs"], dict): + raise ValueError("dataset_configs must be of object type") + + if config["dataset_configs"]['retrieval_model'] == 'multiple': + if not config["dataset_configs"]['reranking_model']: + raise ValueError("reranking_model has not been set") + if not isinstance(config["dataset_configs"]['reranking_model'], dict): + raise ValueError("reranking_model must be of object type") + + if not isinstance(config["dataset_configs"], dict): + raise ValueError("dataset_configs must be of object type") + + need_manual_query_datasets = config.get("dataset_configs") and config["dataset_configs"].get("datasets") + + if need_manual_query_datasets and app_mode == AppMode.COMPLETION: + # Only check when mode is completion + dataset_query_variable = config.get("dataset_query_variable") + + if not dataset_query_variable: + raise ValueError("Dataset query variable is required when dataset is exist") + + return config, ["agent_mode", "dataset_configs", "dataset_query_variable"] + + @classmethod + def extract_dataset_config_for_legacy_compatibility(cls, tenant_id: str, app_mode: AppMode, config: dict) -> dict: + """ + Extract dataset config for legacy compatibility + + :param tenant_id: tenant ID + :param app_mode: app mode + :param config: app model config args + """ + # Extract dataset config for legacy compatibility + if not config.get("agent_mode"): + config["agent_mode"] = { + "enabled": False, + "tools": [] + } + + if not isinstance(config["agent_mode"], dict): + raise ValueError("agent_mode must be of object type") + + # enabled + if "enabled" not in config["agent_mode"] or not config["agent_mode"]["enabled"]: + config["agent_mode"]["enabled"] = False + + if not isinstance(config["agent_mode"]["enabled"], bool): + raise ValueError("enabled in agent_mode must be of boolean type") + + # tools + if not config["agent_mode"].get("tools"): + config["agent_mode"]["tools"] = [] + + if not isinstance(config["agent_mode"]["tools"], list): + raise ValueError("tools in agent_mode must be a list of objects") + + # strategy + if not config["agent_mode"].get("strategy"): + config["agent_mode"]["strategy"] = PlanningStrategy.ROUTER.value + + has_datasets = False + if config["agent_mode"]["strategy"] in [PlanningStrategy.ROUTER.value, PlanningStrategy.REACT_ROUTER.value]: + for tool in config["agent_mode"]["tools"]: + key = list(tool.keys())[0] + if key == "dataset": + # old style, use tool name as key + tool_item = tool[key] + + if "enabled" not in tool_item or not tool_item["enabled"]: + tool_item["enabled"] = False + + if not isinstance(tool_item["enabled"], bool): + raise ValueError("enabled in agent_mode.tools must be of boolean type") + + if 'id' not in tool_item: + raise ValueError("id is required in dataset") + + try: + uuid.UUID(tool_item["id"]) + except ValueError: + raise ValueError("id in dataset must be of UUID type") + + if not cls.is_dataset_exists(tenant_id, tool_item["id"]): + raise ValueError("Dataset ID does not exist, please check your permission.") + + has_datasets = True + + need_manual_query_datasets = has_datasets and config["agent_mode"]["enabled"] + + if need_manual_query_datasets and app_mode == AppMode.COMPLETION: + # Only check when mode is completion + dataset_query_variable = config.get("dataset_query_variable") + + if not dataset_query_variable: + raise ValueError("Dataset query variable is required when dataset is exist") + + return config + + @classmethod + def is_dataset_exists(cls, tenant_id: str, dataset_id: str) -> bool: + # verify if the dataset ID exists + dataset = DatasetService.get_dataset(dataset_id) + + if not dataset: + return False + + if dataset.tenant_id != tenant_id: + return False + + return True diff --git a/api/core/apps/config_validators/external_data_tools.py b/api/core/apps/config_validators/external_data_tools.py new file mode 100644 index 0000000000..5412366a89 --- /dev/null +++ b/api/core/apps/config_validators/external_data_tools.py @@ -0,0 +1,40 @@ +from typing import Tuple + +from core.external_data_tool.factory import ExternalDataToolFactory + + +class ExternalDataToolsValidator: + @classmethod + def validate_and_set_defaults(cls, tenant_id: str, config: dict) -> Tuple[dict, list[str]]: + """ + Validate and set defaults for external data fetch feature + + :param tenant_id: workspace id + :param config: app model config args + """ + if not config.get("external_data_tools"): + config["external_data_tools"] = [] + + if not isinstance(config["external_data_tools"], list): + raise ValueError("external_data_tools must be of list type") + + for tool in config["external_data_tools"]: + if "enabled" not in tool or not tool["enabled"]: + tool["enabled"] = False + + if not tool["enabled"]: + continue + + if "type" not in tool or not tool["type"]: + raise ValueError("external_data_tools[].type is required") + + typ = tool["type"] + config = tool["config"] + + ExternalDataToolFactory.validate_config( + name=typ, + tenant_id=tenant_id, + config=config + ) + + return config, ["external_data_tools"] diff --git a/api/core/apps/config_validators/file_upload.py b/api/core/apps/config_validators/file_upload.py new file mode 100644 index 0000000000..f9adbfdf7d --- /dev/null +++ b/api/core/apps/config_validators/file_upload.py @@ -0,0 +1,38 @@ +from typing import Tuple + + +class FileUploadValidator: + @classmethod + def validate_and_set_defaults(cls, config: dict) -> Tuple[dict, list[str]]: + """ + Validate and set defaults for file upload feature + + :param config: app model config args + """ + if not config.get("file_upload"): + config["file_upload"] = {} + + if not isinstance(config["file_upload"], dict): + raise ValueError("file_upload must be of dict type") + + # check image config + if not config["file_upload"].get("image"): + config["file_upload"]["image"] = {"enabled": False} + + if config['file_upload']['image']['enabled']: + number_limits = config['file_upload']['image']['number_limits'] + if number_limits < 1 or number_limits > 6: + raise ValueError("number_limits must be in [1, 6]") + + detail = config['file_upload']['image']['detail'] + if detail not in ['high', 'low']: + raise ValueError("detail must be in ['high', 'low']") + + transfer_methods = config['file_upload']['image']['transfer_methods'] + if not isinstance(transfer_methods, list): + raise ValueError("transfer_methods must be of list type") + for method in transfer_methods: + if method not in ['remote_url', 'local_file']: + raise ValueError("transfer_methods must be in ['remote_url', 'local_file']") + + return config, ["file_upload"] diff --git a/api/core/apps/config_validators/model.py b/api/core/apps/config_validators/model.py new file mode 100644 index 0000000000..091eec4683 --- /dev/null +++ b/api/core/apps/config_validators/model.py @@ -0,0 +1,83 @@ +from typing import Tuple + +from core.model_runtime.entities.model_entities import ModelType, ModelPropertyKey +from core.model_runtime.model_providers import model_provider_factory +from core.provider_manager import ProviderManager + + +class ModelValidator: + @classmethod + def validate_and_set_defaults(cls, tenant_id: str, config: dict) -> Tuple[dict, list[str]]: + """ + Validate and set defaults for model config + + :param tenant_id: tenant id + :param config: app model config args + """ + if 'model' not in config: + raise ValueError("model is required") + + if not isinstance(config["model"], dict): + raise ValueError("model must be of object type") + + # model.provider + provider_entities = model_provider_factory.get_providers() + model_provider_names = [provider.provider for provider in provider_entities] + if 'provider' not in config["model"] or config["model"]["provider"] not in model_provider_names: + raise ValueError(f"model.provider is required and must be in {str(model_provider_names)}") + + # model.name + if 'name' not in config["model"]: + raise ValueError("model.name is required") + + provider_manager = ProviderManager() + models = provider_manager.get_configurations(tenant_id).get_models( + provider=config["model"]["provider"], + model_type=ModelType.LLM + ) + + if not models: + raise ValueError("model.name must be in the specified model list") + + model_ids = [m.model for m in models] + if config["model"]["name"] not in model_ids: + raise ValueError("model.name must be in the specified model list") + + model_mode = None + for model in models: + if model.model == config["model"]["name"]: + model_mode = model.model_properties.get(ModelPropertyKey.MODE) + break + + # model.mode + if model_mode: + config['model']["mode"] = model_mode + else: + config['model']["mode"] = "completion" + + # model.completion_params + if 'completion_params' not in config["model"]: + raise ValueError("model.completion_params is required") + + config["model"]["completion_params"] = cls.validate_model_completion_params( + config["model"]["completion_params"] + ) + + return config, ["model"] + + @classmethod + def validate_model_completion_params(cls, cp: dict) -> dict: + # model.completion_params + if not isinstance(cp, dict): + raise ValueError("model.completion_params must be of object type") + + # stop + if 'stop' not in cp: + cp["stop"] = [] + elif not isinstance(cp["stop"], list): + raise ValueError("stop in model.completion_params must be of list type") + + if len(cp["stop"]) > 4: + raise ValueError("stop sequences must be less than 4") + + return cp diff --git a/api/core/apps/config_validators/moderation.py b/api/core/apps/config_validators/moderation.py new file mode 100644 index 0000000000..1962f87aa9 --- /dev/null +++ b/api/core/apps/config_validators/moderation.py @@ -0,0 +1,36 @@ +import logging +from typing import Tuple + +from core.moderation.factory import ModerationFactory + +logger = logging.getLogger(__name__) + + +class ModerationValidator: + @classmethod + def validate_and_set_defaults(cls, tenant_id, config: dict) -> Tuple[dict, list[str]]: + if not config.get("sensitive_word_avoidance"): + config["sensitive_word_avoidance"] = { + "enabled": False + } + + if not isinstance(config["sensitive_word_avoidance"], dict): + raise ValueError("sensitive_word_avoidance must be of dict type") + + if "enabled" not in config["sensitive_word_avoidance"] or not config["sensitive_word_avoidance"]["enabled"]: + config["sensitive_word_avoidance"]["enabled"] = False + + if config["sensitive_word_avoidance"]["enabled"]: + if not config["sensitive_word_avoidance"].get("type"): + raise ValueError("sensitive_word_avoidance.type is required") + + typ = config["sensitive_word_avoidance"]["type"] + config = config["sensitive_word_avoidance"]["config"] + + ModerationFactory.validate_config( + name=typ, + tenant_id=tenant_id, + config=config + ) + + return config, ["sensitive_word_avoidance"] diff --git a/api/core/apps/config_validators/more_like_this.py b/api/core/apps/config_validators/more_like_this.py new file mode 100644 index 0000000000..60dc4a0562 --- /dev/null +++ b/api/core/apps/config_validators/more_like_this.py @@ -0,0 +1,26 @@ +from typing import Tuple + + +class MoreLikeThisValidator: + @classmethod + def validate_and_set_defaults(cls, config: dict) -> Tuple[dict, list[str]]: + """ + Validate and set defaults for more like this feature + + :param config: app model config args + """ + if not config.get("more_like_this"): + config["more_like_this"] = { + "enabled": False + } + + if not isinstance(config["more_like_this"], dict): + raise ValueError("more_like_this must be of dict type") + + if "enabled" not in config["more_like_this"] or not config["more_like_this"]["enabled"]: + config["more_like_this"]["enabled"] = False + + if not isinstance(config["more_like_this"]["enabled"], bool): + raise ValueError("enabled in more_like_this must be of boolean type") + + return config, ["more_like_this"] diff --git a/api/core/apps/config_validators/opening_statement.py b/api/core/apps/config_validators/opening_statement.py new file mode 100644 index 0000000000..3f69e0e946 --- /dev/null +++ b/api/core/apps/config_validators/opening_statement.py @@ -0,0 +1,29 @@ +from typing import Tuple + + +class OpeningStatementValidator: + @classmethod + def validate_and_set_defaults(cls, config: dict) -> Tuple[dict, list[str]]: + """ + Validate and set defaults for opening statement feature + + :param config: app model config args + """ + if not config.get("opening_statement"): + config["opening_statement"] = "" + + if not isinstance(config["opening_statement"], str): + raise ValueError("opening_statement must be of string type") + + # suggested_questions + if not config.get("suggested_questions"): + config["suggested_questions"] = [] + + if not isinstance(config["suggested_questions"], list): + raise ValueError("suggested_questions must be of list type") + + for question in config["suggested_questions"]: + if not isinstance(question, str): + raise ValueError("Elements in suggested_questions list must be of string type") + + return config, ["opening_statement", "suggested_questions"] diff --git a/api/core/apps/config_validators/prompt.py b/api/core/apps/config_validators/prompt.py new file mode 100644 index 0000000000..815706b10b --- /dev/null +++ b/api/core/apps/config_validators/prompt.py @@ -0,0 +1,87 @@ +from typing import Tuple + +from core.entities.application_entities import PromptTemplateEntity +from core.prompt.simple_prompt_transform import ModelMode +from models.model import AppMode + + +class PromptValidator: + @classmethod + def validate_and_set_defaults(cls, app_mode: AppMode, config: dict) -> Tuple[dict, list[str]]: + """ + Validate pre_prompt and set defaults for prompt feature + depending on the config['model'] + + :param app_mode: app mode + :param config: app model config args + """ + if not config.get("prompt_type"): + config["prompt_type"] = PromptTemplateEntity.PromptType.SIMPLE.value + + prompt_type_vals = [typ.value for typ in PromptTemplateEntity.PromptType] + if config['prompt_type'] not in prompt_type_vals: + raise ValueError(f"prompt_type must be in {prompt_type_vals}") + + # chat_prompt_config + if not config.get("chat_prompt_config"): + config["chat_prompt_config"] = {} + + if not isinstance(config["chat_prompt_config"], dict): + raise ValueError("chat_prompt_config must be of object type") + + # completion_prompt_config + if not config.get("completion_prompt_config"): + config["completion_prompt_config"] = {} + + if not isinstance(config["completion_prompt_config"], dict): + raise ValueError("completion_prompt_config must be of object type") + + if config['prompt_type'] == PromptTemplateEntity.PromptType.ADVANCED.value: + if not config['chat_prompt_config'] and not config['completion_prompt_config']: + raise ValueError("chat_prompt_config or completion_prompt_config is required " + "when prompt_type is advanced") + + model_mode_vals = [mode.value for mode in ModelMode] + if config['model']["mode"] not in model_mode_vals: + raise ValueError(f"model.mode must be in {model_mode_vals} when prompt_type is advanced") + + if app_mode == AppMode.CHAT and config['model']["mode"] == ModelMode.COMPLETION.value: + user_prefix = config['completion_prompt_config']['conversation_histories_role']['user_prefix'] + assistant_prefix = config['completion_prompt_config']['conversation_histories_role']['assistant_prefix'] + + if not user_prefix: + config['completion_prompt_config']['conversation_histories_role']['user_prefix'] = 'Human' + + if not assistant_prefix: + config['completion_prompt_config']['conversation_histories_role']['assistant_prefix'] = 'Assistant' + + if config['model']["mode"] == ModelMode.CHAT.value: + prompt_list = config['chat_prompt_config']['prompt'] + + if len(prompt_list) > 10: + raise ValueError("prompt messages must be less than 10") + else: + # pre_prompt, for simple mode + if not config.get("pre_prompt"): + config["pre_prompt"] = "" + + if not isinstance(config["pre_prompt"], str): + raise ValueError("pre_prompt must be of string type") + + return config, ["prompt_type", "pre_prompt", "chat_prompt_config", "completion_prompt_config"] + + @classmethod + def validate_post_prompt_and_set_defaults(cls, config: dict) -> dict: + """ + Validate post_prompt and set defaults for prompt feature + + :param config: app model config args + """ + # post_prompt + if not config.get("post_prompt"): + config["post_prompt"] = "" + + if not isinstance(config["post_prompt"], str): + raise ValueError("post_prompt must be of string type") + + return config \ No newline at end of file diff --git a/api/core/apps/config_validators/retriever_resource.py b/api/core/apps/config_validators/retriever_resource.py new file mode 100644 index 0000000000..a8bcd60abe --- /dev/null +++ b/api/core/apps/config_validators/retriever_resource.py @@ -0,0 +1,26 @@ +from typing import Tuple + + +class RetrieverResourceValidator: + @classmethod + def validate_and_set_defaults(cls, config: dict) -> Tuple[dict, list[str]]: + """ + Validate and set defaults for retriever resource feature + + :param config: app model config args + """ + if not config.get("retriever_resource"): + config["retriever_resource"] = { + "enabled": False + } + + if not isinstance(config["retriever_resource"], dict): + raise ValueError("retriever_resource must be of dict type") + + if "enabled" not in config["retriever_resource"] or not config["retriever_resource"]["enabled"]: + config["retriever_resource"]["enabled"] = False + + if not isinstance(config["retriever_resource"]["enabled"], bool): + raise ValueError("enabled in retriever_resource must be of boolean type") + + return config, ["retriever_resource"] diff --git a/api/core/apps/config_validators/speech_to_text.py b/api/core/apps/config_validators/speech_to_text.py new file mode 100644 index 0000000000..577bef0e59 --- /dev/null +++ b/api/core/apps/config_validators/speech_to_text.py @@ -0,0 +1,26 @@ +from typing import Tuple + + +class SpeechToTextValidator: + @classmethod + def validate_and_set_defaults(cls, config: dict) -> Tuple[dict, list[str]]: + """ + Validate and set defaults for speech to text feature + + :param config: app model config args + """ + if not config.get("speech_to_text"): + config["speech_to_text"] = { + "enabled": False + } + + if not isinstance(config["speech_to_text"], dict): + raise ValueError("speech_to_text must be of dict type") + + if "enabled" not in config["speech_to_text"] or not config["speech_to_text"]["enabled"]: + config["speech_to_text"]["enabled"] = False + + if not isinstance(config["speech_to_text"]["enabled"], bool): + raise ValueError("enabled in speech_to_text must be of boolean type") + + return config, ["speech_to_text"] diff --git a/api/core/apps/config_validators/suggested_questions.py b/api/core/apps/config_validators/suggested_questions.py new file mode 100644 index 0000000000..938b66bb6e --- /dev/null +++ b/api/core/apps/config_validators/suggested_questions.py @@ -0,0 +1,26 @@ +from typing import Tuple + + +class SuggestedQuestionsValidator: + @classmethod + def validate_and_set_defaults(cls, config: dict) -> Tuple[dict, list[str]]: + """ + Validate and set defaults for suggested questions feature + + :param config: app model config args + """ + if not config.get("suggested_questions_after_answer"): + config["suggested_questions_after_answer"] = { + "enabled": False + } + + if not isinstance(config["suggested_questions_after_answer"], dict): + raise ValueError("suggested_questions_after_answer must be of dict type") + + if "enabled" not in config["suggested_questions_after_answer"] or not config["suggested_questions_after_answer"]["enabled"]: + config["suggested_questions_after_answer"]["enabled"] = False + + if not isinstance(config["suggested_questions_after_answer"]["enabled"], bool): + raise ValueError("enabled in suggested_questions_after_answer must be of boolean type") + + return config, ["suggested_questions_after_answer"] diff --git a/api/core/apps/config_validators/text_to_speech.py b/api/core/apps/config_validators/text_to_speech.py new file mode 100644 index 0000000000..efe34a8a3e --- /dev/null +++ b/api/core/apps/config_validators/text_to_speech.py @@ -0,0 +1,30 @@ +from typing import Tuple + + +class TextToSpeechValidator: + @classmethod + def validate_and_set_defaults(cls, config: dict) -> Tuple[dict, list[str]]: + """ + Validate and set defaults for text to speech feature + + :param config: app model config args + """ + if not config.get("text_to_speech"): + config["text_to_speech"] = { + "enabled": False, + "voice": "", + "language": "" + } + + if not isinstance(config["text_to_speech"], dict): + raise ValueError("text_to_speech must be of dict type") + + if "enabled" not in config["text_to_speech"] or not config["text_to_speech"]["enabled"]: + config["text_to_speech"]["enabled"] = False + config["text_to_speech"]["voice"] = "" + config["text_to_speech"]["language"] = "" + + if not isinstance(config["text_to_speech"]["enabled"], bool): + raise ValueError("enabled in text_to_speech must be of boolean type") + + return config, ["text_to_speech"] diff --git a/api/core/apps/config_validators/user_input_form.py b/api/core/apps/config_validators/user_input_form.py new file mode 100644 index 0000000000..7116c55afc --- /dev/null +++ b/api/core/apps/config_validators/user_input_form.py @@ -0,0 +1,62 @@ +import re +from typing import Tuple + + +class UserInputFormValidator: + @classmethod + def validate_and_set_defaults(cls, config: dict) -> Tuple[dict, list[str]]: + """ + Validate and set defaults for user input form + + :param config: app model config args + """ + if not config.get("user_input_form"): + config["user_input_form"] = [] + + if not isinstance(config["user_input_form"], list): + raise ValueError("user_input_form must be a list of objects") + + variables = [] + for item in config["user_input_form"]: + key = list(item.keys())[0] + if key not in ["text-input", "select", "paragraph", "number", "external_data_tool"]: + raise ValueError("Keys in user_input_form list can only be 'text-input', 'paragraph' or 'select'") + + form_item = item[key] + if 'label' not in form_item: + raise ValueError("label is required in user_input_form") + + if not isinstance(form_item["label"], str): + raise ValueError("label in user_input_form must be of string type") + + if 'variable' not in form_item: + raise ValueError("variable is required in user_input_form") + + if not isinstance(form_item["variable"], str): + raise ValueError("variable in user_input_form must be of string type") + + pattern = re.compile(r"^(?!\d)[\u4e00-\u9fa5A-Za-z0-9_\U0001F300-\U0001F64F\U0001F680-\U0001F6FF]{1,100}$") + if pattern.match(form_item["variable"]) is None: + raise ValueError("variable in user_input_form must be a string, " + "and cannot start with a number") + + variables.append(form_item["variable"]) + + if 'required' not in form_item or not form_item["required"]: + form_item["required"] = False + + if not isinstance(form_item["required"], bool): + raise ValueError("required in user_input_form must be of boolean type") + + if key == "select": + if 'options' not in form_item or not form_item["options"]: + form_item["options"] = [] + + if not isinstance(form_item["options"], list): + raise ValueError("options in user_input_form must be a list of strings") + + if "default" in form_item and form_item['default'] \ + and form_item["default"] not in form_item["options"]: + raise ValueError("default value in user_input_form must be in the options list") + + return config, ["user_input_form"] diff --git a/api/services/app_model_config_service.py b/api/services/app_model_config_service.py index 34b6d62d51..c1e0ecebe8 100644 --- a/api/services/app_model_config_service.py +++ b/api/services/app_model_config_service.py @@ -1,528 +1,29 @@ -import re -import uuid - -from core.entities.agent_entities import PlanningStrategy -from core.entities.application_entities import AppMode -from core.external_data_tool.factory import ExternalDataToolFactory -from core.model_runtime.entities.model_entities import ModelPropertyKey, ModelType -from core.model_runtime.model_providers import model_provider_factory -from core.moderation.factory import ModerationFactory -from core.provider_manager import ProviderManager -from models.account import Account +from core.apps.app_config_validators.advanced_chat_app import AdvancedChatAppConfigValidator +from core.apps.app_config_validators.agent_chat_app import AgentChatAppConfigValidator +from core.apps.app_config_validators.chat_app import ChatAppConfigValidator +from core.apps.app_config_validators.completion_app import CompletionAppConfigValidator +from core.apps.app_config_validators.workflow_app import WorkflowAppConfigValidator from models.model import AppMode -from services.dataset_service import DatasetService - -SUPPORT_TOOLS = ["dataset", "google_search", "web_reader", "wikipedia", "current_datetime"] class AppModelConfigService: - @classmethod - def is_dataset_exists(cls, account: Account, dataset_id: str) -> bool: - # verify if the dataset ID exists - dataset = DatasetService.get_dataset(dataset_id) - - if not dataset: - return False - - if dataset.tenant_id != account.current_tenant_id: - return False - - return True @classmethod - def validate_model_completion_params(cls, cp: dict, model_name: str) -> dict: - # 6. model.completion_params - if not isinstance(cp, dict): - raise ValueError("model.completion_params must be of object type") - - # stop - if 'stop' not in cp: - cp["stop"] = [] - elif not isinstance(cp["stop"], list): - raise ValueError("stop in model.completion_params must be of list type") - - if len(cp["stop"]) > 4: - raise ValueError("stop sequences must be less than 4") - - return cp - - @classmethod - def validate_configuration(cls, tenant_id: str, account: Account, config: dict, app_mode: str) -> dict: - # opening_statement - if 'opening_statement' not in config or not config["opening_statement"]: - config["opening_statement"] = "" - - if not isinstance(config["opening_statement"], str): - raise ValueError("opening_statement must be of string type") - - # suggested_questions - if 'suggested_questions' not in config or not config["suggested_questions"]: - config["suggested_questions"] = [] - - if not isinstance(config["suggested_questions"], list): - raise ValueError("suggested_questions must be of list type") - - for question in config["suggested_questions"]: - if not isinstance(question, str): - raise ValueError("Elements in suggested_questions list must be of string type") - - # suggested_questions_after_answer - if 'suggested_questions_after_answer' not in config or not config["suggested_questions_after_answer"]: - config["suggested_questions_after_answer"] = { - "enabled": False - } - - if not isinstance(config["suggested_questions_after_answer"], dict): - raise ValueError("suggested_questions_after_answer must be of dict type") - - if "enabled" not in config["suggested_questions_after_answer"] or not config["suggested_questions_after_answer"]["enabled"]: - config["suggested_questions_after_answer"]["enabled"] = False - - if not isinstance(config["suggested_questions_after_answer"]["enabled"], bool): - raise ValueError("enabled in suggested_questions_after_answer must be of boolean type") - - # speech_to_text - if 'speech_to_text' not in config or not config["speech_to_text"]: - config["speech_to_text"] = { - "enabled": False - } - - if not isinstance(config["speech_to_text"], dict): - raise ValueError("speech_to_text must be of dict type") - - if "enabled" not in config["speech_to_text"] or not config["speech_to_text"]["enabled"]: - config["speech_to_text"]["enabled"] = False - - if not isinstance(config["speech_to_text"]["enabled"], bool): - raise ValueError("enabled in speech_to_text must be of boolean type") - - # text_to_speech - if 'text_to_speech' not in config or not config["text_to_speech"]: - config["text_to_speech"] = { - "enabled": False, - "voice": "", - "language": "" - } - - if not isinstance(config["text_to_speech"], dict): - raise ValueError("text_to_speech must be of dict type") - - if "enabled" not in config["text_to_speech"] or not config["text_to_speech"]["enabled"]: - config["text_to_speech"]["enabled"] = False - config["text_to_speech"]["voice"] = "" - config["text_to_speech"]["language"] = "" - - if not isinstance(config["text_to_speech"]["enabled"], bool): - raise ValueError("enabled in text_to_speech must be of boolean type") - - # return retriever resource - if 'retriever_resource' not in config or not config["retriever_resource"]: - config["retriever_resource"] = { - "enabled": False - } - - if not isinstance(config["retriever_resource"], dict): - raise ValueError("retriever_resource must be of dict type") - - if "enabled" not in config["retriever_resource"] or not config["retriever_resource"]["enabled"]: - config["retriever_resource"]["enabled"] = False - - if not isinstance(config["retriever_resource"]["enabled"], bool): - raise ValueError("enabled in retriever_resource must be of boolean type") - - # more_like_this - if 'more_like_this' not in config or not config["more_like_this"]: - config["more_like_this"] = { - "enabled": False - } - - if not isinstance(config["more_like_this"], dict): - raise ValueError("more_like_this must be of dict type") - - if "enabled" not in config["more_like_this"] or not config["more_like_this"]["enabled"]: - config["more_like_this"]["enabled"] = False - - if not isinstance(config["more_like_this"]["enabled"], bool): - raise ValueError("enabled in more_like_this must be of boolean type") - - # model - if 'model' not in config: - raise ValueError("model is required") - - if not isinstance(config["model"], dict): - raise ValueError("model must be of object type") - - # model.provider - provider_entities = model_provider_factory.get_providers() - model_provider_names = [provider.provider for provider in provider_entities] - if 'provider' not in config["model"] or config["model"]["provider"] not in model_provider_names: - raise ValueError(f"model.provider is required and must be in {str(model_provider_names)}") - - # model.name - if 'name' not in config["model"]: - raise ValueError("model.name is required") - - provider_manager = ProviderManager() - models = provider_manager.get_configurations(tenant_id).get_models( - provider=config["model"]["provider"], - model_type=ModelType.LLM - ) - if not models: - raise ValueError("model.name must be in the specified model list") - - model_ids = [m.model for m in models] - if config["model"]["name"] not in model_ids: - raise ValueError("model.name must be in the specified model list") - - model_mode = None - for model in models: - if model.model == config["model"]["name"]: - model_mode = model.model_properties.get(ModelPropertyKey.MODE) - break - - # model.mode - if model_mode: - config['model']["mode"] = model_mode + def validate_configuration(cls, tenant_id: str, config: dict, app_mode: AppMode) -> dict: + if app_mode == AppMode.CHAT: + return ChatAppConfigValidator.config_validate(tenant_id, config) + elif app_mode == AppMode.AGENT_CHAT: + return AgentChatAppConfigValidator.config_validate(tenant_id, config) + elif app_mode == AppMode.COMPLETION: + return CompletionAppConfigValidator.config_validate(tenant_id, config) else: - config['model']["mode"] = "completion" - - # model.completion_params - if 'completion_params' not in config["model"]: - raise ValueError("model.completion_params is required") - - config["model"]["completion_params"] = cls.validate_model_completion_params( - config["model"]["completion_params"], - config["model"]["name"] - ) - - # user_input_form - if "user_input_form" not in config or not config["user_input_form"]: - config["user_input_form"] = [] - - if not isinstance(config["user_input_form"], list): - raise ValueError("user_input_form must be a list of objects") - - variables = [] - for item in config["user_input_form"]: - key = list(item.keys())[0] - if key not in ["text-input", "select", "paragraph", "number", "external_data_tool"]: - raise ValueError("Keys in user_input_form list can only be 'text-input', 'paragraph' or 'select'") - - form_item = item[key] - if 'label' not in form_item: - raise ValueError("label is required in user_input_form") - - if not isinstance(form_item["label"], str): - raise ValueError("label in user_input_form must be of string type") - - if 'variable' not in form_item: - raise ValueError("variable is required in user_input_form") - - if not isinstance(form_item["variable"], str): - raise ValueError("variable in user_input_form must be of string type") - - pattern = re.compile(r"^(?!\d)[\u4e00-\u9fa5A-Za-z0-9_\U0001F300-\U0001F64F\U0001F680-\U0001F6FF]{1,100}$") - if pattern.match(form_item["variable"]) is None: - raise ValueError("variable in user_input_form must be a string, " - "and cannot start with a number") - - variables.append(form_item["variable"]) - - if 'required' not in form_item or not form_item["required"]: - form_item["required"] = False - - if not isinstance(form_item["required"], bool): - raise ValueError("required in user_input_form must be of boolean type") - - if key == "select": - if 'options' not in form_item or not form_item["options"]: - form_item["options"] = [] - - if not isinstance(form_item["options"], list): - raise ValueError("options in user_input_form must be a list of strings") - - if "default" in form_item and form_item['default'] \ - and form_item["default"] not in form_item["options"]: - raise ValueError("default value in user_input_form must be in the options list") - - # pre_prompt - if "pre_prompt" not in config or not config["pre_prompt"]: - config["pre_prompt"] = "" - - if not isinstance(config["pre_prompt"], str): - raise ValueError("pre_prompt must be of string type") - - # agent_mode - if "agent_mode" not in config or not config["agent_mode"]: - config["agent_mode"] = { - "enabled": False, - "tools": [] - } - - if not isinstance(config["agent_mode"], dict): - raise ValueError("agent_mode must be of object type") - - if "enabled" not in config["agent_mode"] or not config["agent_mode"]["enabled"]: - config["agent_mode"]["enabled"] = False - - if not isinstance(config["agent_mode"]["enabled"], bool): - raise ValueError("enabled in agent_mode must be of boolean type") - - if "strategy" not in config["agent_mode"] or not config["agent_mode"]["strategy"]: - config["agent_mode"]["strategy"] = PlanningStrategy.ROUTER.value - - if config["agent_mode"]["strategy"] not in [member.value for member in list(PlanningStrategy.__members__.values())]: - raise ValueError("strategy in agent_mode must be in the specified strategy list") - - if "tools" not in config["agent_mode"] or not config["agent_mode"]["tools"]: - config["agent_mode"]["tools"] = [] - - if not isinstance(config["agent_mode"]["tools"], list): - raise ValueError("tools in agent_mode must be a list of objects") - - for tool in config["agent_mode"]["tools"]: - key = list(tool.keys())[0] - if key in SUPPORT_TOOLS: - # old style, use tool name as key - tool_item = tool[key] - - if "enabled" not in tool_item or not tool_item["enabled"]: - tool_item["enabled"] = False - - if not isinstance(tool_item["enabled"], bool): - raise ValueError("enabled in agent_mode.tools must be of boolean type") - - if key == "dataset": - if 'id' not in tool_item: - raise ValueError("id is required in dataset") - - try: - uuid.UUID(tool_item["id"]) - except ValueError: - raise ValueError("id in dataset must be of UUID type") - - if not cls.is_dataset_exists(account, tool_item["id"]): - raise ValueError("Dataset ID does not exist, please check your permission.") - else: - # latest style, use key-value pair - if "enabled" not in tool or not tool["enabled"]: - tool["enabled"] = False - if "provider_type" not in tool: - raise ValueError("provider_type is required in agent_mode.tools") - if "provider_id" not in tool: - raise ValueError("provider_id is required in agent_mode.tools") - if "tool_name" not in tool: - raise ValueError("tool_name is required in agent_mode.tools") - if "tool_parameters" not in tool: - raise ValueError("tool_parameters is required in agent_mode.tools") - - # dataset_query_variable - cls.is_dataset_query_variable_valid(config, app_mode) - - # advanced prompt validation - cls.is_advanced_prompt_valid(config, app_mode) - - # external data tools validation - cls.is_external_data_tools_valid(tenant_id, config) - - # moderation validation - cls.is_moderation_valid(tenant_id, config) - - # file upload validation - cls.is_file_upload_valid(config) - - # Filter out extra parameters - filtered_config = { - "opening_statement": config["opening_statement"], - "suggested_questions": config["suggested_questions"], - "suggested_questions_after_answer": config["suggested_questions_after_answer"], - "speech_to_text": config["speech_to_text"], - "text_to_speech": config["text_to_speech"], - "retriever_resource": config["retriever_resource"], - "more_like_this": config["more_like_this"], - "sensitive_word_avoidance": config["sensitive_word_avoidance"], - "external_data_tools": config["external_data_tools"], - "model": { - "provider": config["model"]["provider"], - "name": config["model"]["name"], - "mode": config['model']["mode"], - "completion_params": config["model"]["completion_params"] - }, - "user_input_form": config["user_input_form"], - "dataset_query_variable": config.get('dataset_query_variable'), - "pre_prompt": config["pre_prompt"], - "agent_mode": config["agent_mode"], - "prompt_type": config["prompt_type"], - "chat_prompt_config": config["chat_prompt_config"], - "completion_prompt_config": config["completion_prompt_config"], - "dataset_configs": config["dataset_configs"], - "file_upload": config["file_upload"] - } - - return filtered_config + raise ValueError(f"Invalid app mode: {app_mode}") @classmethod - def is_moderation_valid(cls, tenant_id: str, config: dict): - if 'sensitive_word_avoidance' not in config or not config["sensitive_word_avoidance"]: - config["sensitive_word_avoidance"] = { - "enabled": False - } - - if not isinstance(config["sensitive_word_avoidance"], dict): - raise ValueError("sensitive_word_avoidance must be of dict type") - - if "enabled" not in config["sensitive_word_avoidance"] or not config["sensitive_word_avoidance"]["enabled"]: - config["sensitive_word_avoidance"]["enabled"] = False - - if not config["sensitive_word_avoidance"]["enabled"]: - return - - if "type" not in config["sensitive_word_avoidance"] or not config["sensitive_word_avoidance"]["type"]: - raise ValueError("sensitive_word_avoidance.type is required") - - type = config["sensitive_word_avoidance"]["type"] - config = config["sensitive_word_avoidance"]["config"] - - ModerationFactory.validate_config( - name=type, - tenant_id=tenant_id, - config=config - ) - - @classmethod - def is_file_upload_valid(cls, config: dict): - if 'file_upload' not in config or not config["file_upload"]: - config["file_upload"] = {} - - if not isinstance(config["file_upload"], dict): - raise ValueError("file_upload must be of dict type") - - # check image config - if 'image' not in config["file_upload"] or not config["file_upload"]["image"]: - config["file_upload"]["image"] = {"enabled": False} - - if config['file_upload']['image']['enabled']: - number_limits = config['file_upload']['image']['number_limits'] - if number_limits < 1 or number_limits > 6: - raise ValueError("number_limits must be in [1, 6]") - - detail = config['file_upload']['image']['detail'] - if detail not in ['high', 'low']: - raise ValueError("detail must be in ['high', 'low']") - - transfer_methods = config['file_upload']['image']['transfer_methods'] - if not isinstance(transfer_methods, list): - raise ValueError("transfer_methods must be of list type") - for method in transfer_methods: - if method not in ['remote_url', 'local_file']: - raise ValueError("transfer_methods must be in ['remote_url', 'local_file']") - - @classmethod - def is_external_data_tools_valid(cls, tenant_id: str, config: dict): - if 'external_data_tools' not in config or not config["external_data_tools"]: - config["external_data_tools"] = [] - - if not isinstance(config["external_data_tools"], list): - raise ValueError("external_data_tools must be of list type") - - for tool in config["external_data_tools"]: - if "enabled" not in tool or not tool["enabled"]: - tool["enabled"] = False - - if not tool["enabled"]: - continue - - if "type" not in tool or not tool["type"]: - raise ValueError("external_data_tools[].type is required") - - type = tool["type"] - config = tool["config"] - - ExternalDataToolFactory.validate_config( - name=type, - tenant_id=tenant_id, - config=config - ) - - @classmethod - def is_dataset_query_variable_valid(cls, config: dict, mode: str) -> None: - # Only check when mode is completion - if mode != 'completion': - return - - agent_mode = config.get("agent_mode", {}) - tools = agent_mode.get("tools", []) - dataset_exists = "dataset" in str(tools) - - dataset_query_variable = config.get("dataset_query_variable") - - if dataset_exists and not dataset_query_variable: - raise ValueError("Dataset query variable is required when dataset is exist") - - @classmethod - def is_advanced_prompt_valid(cls, config: dict, app_mode: str) -> None: - # prompt_type - if 'prompt_type' not in config or not config["prompt_type"]: - config["prompt_type"] = "simple" - - if config['prompt_type'] not in ['simple', 'advanced']: - raise ValueError("prompt_type must be in ['simple', 'advanced']") - - # chat_prompt_config - if 'chat_prompt_config' not in config or not config["chat_prompt_config"]: - config["chat_prompt_config"] = {} - - if not isinstance(config["chat_prompt_config"], dict): - raise ValueError("chat_prompt_config must be of object type") - - # completion_prompt_config - if 'completion_prompt_config' not in config or not config["completion_prompt_config"]: - config["completion_prompt_config"] = {} - - if not isinstance(config["completion_prompt_config"], dict): - raise ValueError("completion_prompt_config must be of object type") - - # dataset_configs - if 'dataset_configs' not in config or not config["dataset_configs"]: - config["dataset_configs"] = {'retrieval_model': 'single'} - - if 'datasets' not in config["dataset_configs"] or not config["dataset_configs"]["datasets"]: - config["dataset_configs"]["datasets"] = { - "strategy": "router", - "datasets": [] - } - - if not isinstance(config["dataset_configs"], dict): - raise ValueError("dataset_configs must be of object type") - - if config["dataset_configs"]['retrieval_model'] == 'multiple': - if not config["dataset_configs"]['reranking_model']: - raise ValueError("reranking_model has not been set") - if not isinstance(config["dataset_configs"]['reranking_model'], dict): - raise ValueError("reranking_model must be of object type") - - if not isinstance(config["dataset_configs"], dict): - raise ValueError("dataset_configs must be of object type") - - if config['prompt_type'] == 'advanced': - if not config['chat_prompt_config'] and not config['completion_prompt_config']: - raise ValueError("chat_prompt_config or completion_prompt_config is required when prompt_type is advanced") - - if config['model']["mode"] not in ['chat', 'completion']: - raise ValueError("model.mode must be in ['chat', 'completion'] when prompt_type is advanced") - - if app_mode == AppMode.CHAT.value and config['model']["mode"] == "completion": - user_prefix = config['completion_prompt_config']['conversation_histories_role']['user_prefix'] - assistant_prefix = config['completion_prompt_config']['conversation_histories_role']['assistant_prefix'] - - if not user_prefix: - config['completion_prompt_config']['conversation_histories_role']['user_prefix'] = 'Human' - - if not assistant_prefix: - config['completion_prompt_config']['conversation_histories_role']['assistant_prefix'] = 'Assistant' - - if config['model']["mode"] == "chat": - prompt_list = config['chat_prompt_config']['prompt'] - - if len(prompt_list) > 10: - raise ValueError("prompt messages must be less than 10") + def validate_features(cls, tenant_id: str, config: dict, app_mode: AppMode) -> dict: + if app_mode == AppMode.ADVANCED_CHAT: + return AdvancedChatAppConfigValidator.config_validate(tenant_id, config) + elif app_mode == AppMode.WORKFLOW: + return WorkflowAppConfigValidator.config_validate(tenant_id, config) + else: + raise ValueError(f"Invalid app mode: {app_mode}") diff --git a/api/services/completion_service.py b/api/services/completion_service.py index cbfbe9ef41..6dd729694b 100644 --- a/api/services/completion_service.py +++ b/api/services/completion_service.py @@ -5,10 +5,11 @@ from typing import Any, Union from sqlalchemy import and_ from core.application_manager import ApplicationManager +from core.apps.config_validators.model import ModelValidator from core.entities.application_entities import InvokeFrom from core.file.message_file_parser import MessageFileParser from extensions.ext_database import db -from models.model import Account, App, AppModelConfig, Conversation, EndUser, Message +from models.model import Account, App, AppModelConfig, Conversation, EndUser, Message, AppMode from services.app_model_config_service import AppModelConfigService from services.errors.app import MoreLikeThisDisabledError from services.errors.app_model_config import AppModelConfigBrokenError @@ -88,9 +89,8 @@ class CompletionService: if 'completion_params' not in args['model_config']['model']: raise ValueError('model_config.model.completion_params is required') - completion_params = AppModelConfigService.validate_model_completion_params( - cp=args['model_config']['model']['completion_params'], - model_name=app_model_config.model_dict["name"] + completion_params = ModelValidator.validate_model_completion_params( + cp=args['model_config']['model']['completion_params'] ) app_model_config_model = app_model_config.model_dict @@ -115,9 +115,8 @@ class CompletionService: # validate config model_config = AppModelConfigService.validate_configuration( tenant_id=app_model.tenant_id, - account=user, config=args['model_config'], - app_mode=app_model.mode + app_mode=AppMode.value_of(app_model.mode) ) app_model_config = AppModelConfig( diff --git a/api/services/workflow_service.py b/api/services/workflow_service.py index ae6e4c46d3..5a9234c70a 100644 --- a/api/services/workflow_service.py +++ b/api/services/workflow_service.py @@ -96,7 +96,7 @@ class WorkflowService: if not draft_workflow: raise ValueError('No valid workflow found.') - # TODO check if the workflow is valid + # TODO check if the workflow is valid, basic check # create new workflow workflow = Workflow(