From 5c7ea08bdf5e8a90d6697233e617013370d4c5f4 Mon Sep 17 00:00:00 2001 From: takatost Date: Sat, 2 Mar 2024 02:40:18 +0800 Subject: [PATCH] refactor apps --- api/controllers/console/app/audio.py | 2 +- api/controllers/console/app/completion.py | 6 +- api/controllers/console/app/conversation.py | 8 +- api/controllers/console/app/message.py | 4 +- api/controllers/console/app/statistic.py | 2 +- api/controllers/console/explore/completion.py | 2 +- api/controllers/console/explore/message.py | 2 +- api/controllers/service_api/app/completion.py | 2 +- api/controllers/web/completion.py | 2 +- api/controllers/web/message.py | 2 +- api/core/agent/base_agent_runner.py | 47 +- api/core/agent/cot_agent_runner.py | 33 +- api/core/agent/entities.py | 61 +++ api/core/agent/fc_agent_runner.py | 14 +- .../app/advanced_chat/config_validator.py | 59 --- .../{advanced_chat => app_config}/__init__.py | 0 .../app/app_config/base_app_config_manager.py | 73 +++ .../common}/__init__.py | 0 .../sensitive_word_avoidance}/__init__.py | 0 .../sensitive_word_avoidance/manager.py} | 19 +- .../easy_ui_based_app}/__init__.py | 0 .../easy_ui_based_app/agent}/__init__.py | 0 .../easy_ui_based_app/agent/manager.py | 79 ++++ .../easy_ui_based_app/dataset}/__init__.py | 0 .../easy_ui_based_app/dataset/manager.py} | 87 +++- .../model_config/__init__.py | 0 .../model_config/converter.py | 104 +++++ .../model_config/manager.py} | 36 +- .../prompt_template/__init__.py | 0 .../prompt_template/manager.py} | 59 ++- .../easy_ui_based_app/variables/__init__.py | 0 .../easy_ui_based_app/variables/manager.py | 184 ++++++++ .../app_config/entities.py} | 167 ++----- api/core/app/app_config/features/__init__.py | 0 .../features/file_upload/__init__.py | 0 .../features/file_upload/manager.py} | 26 +- .../features/more_like_this/__init__.py | 0 .../features/more_like_this/manager.py} | 15 +- .../features/opening_statement/__init__.py | 0 .../features/opening_statement/manager.py} | 18 +- .../features/retrieval_resource/__init__.py | 0 .../features/retrieval_resource/manager.py} | 10 +- .../features/speech_to_text/__init__.py | 0 .../features/speech_to_text/manager.py} | 15 +- .../__init__.py | 0 .../manager.py} | 18 +- .../features/text_to_speech/__init__.py | 0 .../features/text_to_speech/manager.py} | 22 +- .../workflow_ui_based_app/__init__.py | 0 .../variables/__init__.py | 0 .../variables/manager.py | 22 + api/core/app/app_manager.py | 198 +++++--- .../app/app_orchestration_config_converter.py | 421 ------------------ api/core/app/app_queue_manager.py | 4 +- api/core/app/apps/__init__.py | 0 api/core/app/apps/advanced_chat/__init__.py | 0 .../apps/advanced_chat/app_config_manager.py | 94 ++++ api/core/app/apps/agent_chat/__init__.py | 0 .../agent_chat/app_config_manager.py} | 114 +++-- .../app/{ => apps}/agent_chat/app_runner.py | 69 +-- api/core/app/{ => apps}/base_app_runner.py | 35 +- api/core/app/apps/chat/__init__.py | 0 api/core/app/apps/chat/app_config_manager.py | 135 ++++++ api/core/app/{ => apps}/chat/app_runner.py | 61 +-- api/core/app/apps/completion/__init__.py | 0 .../app/apps/completion/app_config_manager.py | 118 +++++ .../app/{ => apps}/completion/app_runner.py | 53 +-- api/core/app/apps/workflow/__init__.py | 0 .../app/apps/workflow/app_config_manager.py | 71 +++ api/core/app/chat/config_validator.py | 82 ---- api/core/app/completion/config_validator.py | 67 --- api/core/app/entities/__init__.py | 0 api/core/app/entities/app_invoke_entities.py | 111 +++++ api/core/{ => app}/entities/queue_entities.py | 0 .../annotation_reply/annotation_reply.py | 2 +- .../hosting_moderation/hosting_moderation.py | 7 +- api/core/app/generate_task_pipeline.py | 22 +- .../app/validators/external_data_fetch.py | 39 -- api/core/app/validators/user_input_form.py | 61 --- api/core/app/workflow/config_validator.py | 39 -- .../agent_loop_gather_callback_handler.py | 262 ----------- .../callback_handler/entity/agent_loop.py | 23 - .../index_tool_callback_handler.py | 2 +- .../external_data_tool/external_data_fetch.py | 2 +- api/core/file/file_obj.py | 5 +- api/core/file/message_file_parser.py | 35 +- api/core/helper/moderation.py | 4 +- api/core/memory/token_buffer_memory.py | 20 +- api/core/moderation/input_moderation.py | 10 +- api/core/prompt/advanced_prompt_transform.py | 15 +- api/core/prompt/prompt_transform.py | 6 +- api/core/prompt/simple_prompt_transform.py | 14 +- .../rag/retrieval/agent/agent_llm_callback.py | 101 ----- api/core/rag/retrieval/agent/llm_chain.py | 7 +- .../agent/multi_dataset_router_agent.py | 6 +- .../structed_multi_dataset_router_agent.py | 4 +- .../retrieval/agent_based_dataset_executor.py | 8 +- api/core/rag/retrieval/dataset_retrieval.py | 5 +- api/core/tools/tool/dataset_retriever_tool.py | 3 +- .../deduct_quota_when_messaeg_created.py | 8 +- ...vider_last_used_at_when_messaeg_created.py | 8 +- api/models/model.py | 12 + api/models/workflow.py | 2 +- api/services/app_model_config_service.py | 12 +- api/services/completion_service.py | 147 ++---- api/services/workflow/workflow_converter.py | 46 +- api/services/workflow_service.py | 8 +- .../prompt/test_advanced_prompt_transform.py | 10 +- .../core/prompt/test_prompt_transform.py | 2 +- .../prompt/test_simple_prompt_transform.py | 6 +- .../workflow/test_workflow_converter.py | 2 +- 111 files changed, 1979 insertions(+), 1819 deletions(-) create mode 100644 api/core/agent/entities.py delete mode 100644 api/core/app/advanced_chat/config_validator.py rename api/core/app/{advanced_chat => app_config}/__init__.py (100%) create mode 100644 api/core/app/app_config/base_app_config_manager.py rename api/core/app/{agent_chat => app_config/common}/__init__.py (100%) rename api/core/app/{chat => app_config/common/sensitive_word_avoidance}/__init__.py (100%) rename api/core/app/{validators/moderation.py => app_config/common/sensitive_word_avoidance/manager.py} (64%) rename api/core/app/{completion => app_config/easy_ui_based_app}/__init__.py (100%) rename api/core/app/{validators => app_config/easy_ui_based_app/agent}/__init__.py (100%) create mode 100644 api/core/app/app_config/easy_ui_based_app/agent/manager.py rename api/core/app/{workflow => app_config/easy_ui_based_app/dataset}/__init__.py (100%) rename api/core/app/{validators/dataset_retrieval.py => app_config/easy_ui_based_app/dataset/manager.py} (63%) create mode 100644 api/core/app/app_config/easy_ui_based_app/model_config/__init__.py create mode 100644 api/core/app/app_config/easy_ui_based_app/model_config/converter.py rename api/core/app/{validators/model_validator.py => app_config/easy_ui_based_app/model_config/manager.py} (73%) create mode 100644 api/core/app/app_config/easy_ui_based_app/prompt_template/__init__.py rename api/core/app/{validators/prompt.py => app_config/easy_ui_based_app/prompt_template/manager.py} (58%) create mode 100644 api/core/app/app_config/easy_ui_based_app/variables/__init__.py create mode 100644 api/core/app/app_config/easy_ui_based_app/variables/manager.py rename api/core/{entities/application_entities.py => app/app_config/entities.py} (61%) create mode 100644 api/core/app/app_config/features/__init__.py create mode 100644 api/core/app/app_config/features/file_upload/__init__.py rename api/core/app/{validators/file_upload.py => app_config/features/file_upload/manager.py} (59%) create mode 100644 api/core/app/app_config/features/more_like_this/__init__.py rename api/core/app/{validators/more_like_this.py => app_config/features/more_like_this/manager.py} (63%) create mode 100644 api/core/app/app_config/features/opening_statement/__init__.py rename api/core/app/{validators/opening_statement.py => app_config/features/opening_statement/manager.py} (66%) create mode 100644 api/core/app/app_config/features/retrieval_resource/__init__.py rename api/core/app/{validators/retriever_resource.py => app_config/features/retrieval_resource/manager.py} (68%) create mode 100644 api/core/app/app_config/features/speech_to_text/__init__.py rename api/core/app/{validators/speech_to_text.py => app_config/features/speech_to_text/manager.py} (63%) create mode 100644 api/core/app/app_config/features/suggested_questions_after_answer/__init__.py rename api/core/app/{validators/suggested_questions.py => app_config/features/suggested_questions_after_answer/manager.py} (57%) create mode 100644 api/core/app/app_config/features/text_to_speech/__init__.py rename api/core/app/{validators/text_to_speech.py => app_config/features/text_to_speech/manager.py} (56%) create mode 100644 api/core/app/app_config/workflow_ui_based_app/__init__.py create mode 100644 api/core/app/app_config/workflow_ui_based_app/variables/__init__.py create mode 100644 api/core/app/app_config/workflow_ui_based_app/variables/manager.py delete mode 100644 api/core/app/app_orchestration_config_converter.py create mode 100644 api/core/app/apps/__init__.py create mode 100644 api/core/app/apps/advanced_chat/__init__.py create mode 100644 api/core/app/apps/advanced_chat/app_config_manager.py create mode 100644 api/core/app/apps/agent_chat/__init__.py rename api/core/app/{agent_chat/config_validator.py => apps/agent_chat/app_config_manager.py} (51%) rename api/core/app/{ => apps}/agent_chat/app_runner.py (83%) rename api/core/app/{ => apps}/base_app_runner.py (93%) create mode 100644 api/core/app/apps/chat/__init__.py create mode 100644 api/core/app/apps/chat/app_config_manager.py rename api/core/app/{ => apps}/chat/app_runner.py (76%) create mode 100644 api/core/app/apps/completion/__init__.py create mode 100644 api/core/app/apps/completion/app_config_manager.py rename api/core/app/{ => apps}/completion/app_runner.py (74%) create mode 100644 api/core/app/apps/workflow/__init__.py create mode 100644 api/core/app/apps/workflow/app_config_manager.py delete mode 100644 api/core/app/chat/config_validator.py delete mode 100644 api/core/app/completion/config_validator.py create mode 100644 api/core/app/entities/__init__.py create mode 100644 api/core/app/entities/app_invoke_entities.py rename api/core/{ => app}/entities/queue_entities.py (100%) delete mode 100644 api/core/app/validators/external_data_fetch.py delete mode 100644 api/core/app/validators/user_input_form.py delete mode 100644 api/core/app/workflow/config_validator.py delete mode 100644 api/core/callback_handler/agent_loop_gather_callback_handler.py delete mode 100644 api/core/callback_handler/entity/agent_loop.py delete mode 100644 api/core/rag/retrieval/agent/agent_llm_callback.py diff --git a/api/controllers/console/app/audio.py b/api/controllers/console/app/audio.py index c7f3a598ca..4de4a6f3fe 100644 --- a/api/controllers/console/app/audio.py +++ b/api/controllers/console/app/audio.py @@ -37,7 +37,7 @@ class ChatMessageAudioApi(Resource): @setup_required @login_required @account_initialization_required - @get_app_model(mode=AppMode.CHAT) + @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT]) def post(self, app_model): file = request.files['file'] diff --git a/api/controllers/console/app/completion.py b/api/controllers/console/app/completion.py index 0632c0439b..ed1522c0cd 100644 --- a/api/controllers/console/app/completion.py +++ b/api/controllers/console/app/completion.py @@ -22,7 +22,7 @@ from controllers.console.app.wraps import get_app_model from controllers.console.setup import setup_required from controllers.console.wraps import account_initialization_required from core.app.app_queue_manager import AppQueueManager -from core.entities.application_entities import InvokeFrom +from core.app.entities.app_invoke_entities import InvokeFrom from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError from core.model_runtime.errors.invoke import InvokeError from libs.helper import uuid_value @@ -103,7 +103,7 @@ class ChatMessageApi(Resource): @setup_required @login_required @account_initialization_required - @get_app_model(mode=AppMode.CHAT) + @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT]) def post(self, app_model): parser = reqparse.RequestParser() parser.add_argument('inputs', type=dict, required=True, location='json') @@ -168,7 +168,7 @@ class ChatMessageStopApi(Resource): @setup_required @login_required @account_initialization_required - @get_app_model(mode=AppMode.CHAT) + @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT]) def post(self, app_model, task_id): account = flask_login.current_user diff --git a/api/controllers/console/app/conversation.py b/api/controllers/console/app/conversation.py index b808d62eb0..33711076f8 100644 --- a/api/controllers/console/app/conversation.py +++ b/api/controllers/console/app/conversation.py @@ -112,7 +112,7 @@ class CompletionConversationDetailApi(Resource): @setup_required @login_required @account_initialization_required - @get_app_model(mode=AppMode.CHAT) + @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT]) def delete(self, app_model, conversation_id): conversation_id = str(conversation_id) @@ -133,7 +133,7 @@ class ChatConversationApi(Resource): @setup_required @login_required @account_initialization_required - @get_app_model(mode=AppMode.CHAT) + @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT]) @marshal_with(conversation_with_summary_pagination_fields) def get(self, app_model): parser = reqparse.RequestParser() @@ -218,7 +218,7 @@ class ChatConversationDetailApi(Resource): @setup_required @login_required @account_initialization_required - @get_app_model(mode=AppMode.CHAT) + @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT]) @marshal_with(conversation_detail_fields) def get(self, app_model, conversation_id): conversation_id = str(conversation_id) @@ -227,7 +227,7 @@ class ChatConversationDetailApi(Resource): @setup_required @login_required - @get_app_model(mode=AppMode.CHAT) + @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT]) @account_initialization_required def delete(self, app_model, conversation_id): conversation_id = str(conversation_id) diff --git a/api/controllers/console/app/message.py b/api/controllers/console/app/message.py index c384e878aa..111ec7d787 100644 --- a/api/controllers/console/app/message.py +++ b/api/controllers/console/app/message.py @@ -42,7 +42,7 @@ class ChatMessageListApi(Resource): @setup_required @login_required - @get_app_model(mode=AppMode.CHAT) + @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT]) @account_initialization_required @marshal_with(message_infinite_scroll_pagination_fields) def get(self, app_model): @@ -194,7 +194,7 @@ class MessageSuggestedQuestionApi(Resource): @setup_required @login_required @account_initialization_required - @get_app_model(mode=AppMode.CHAT) + @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT]) def get(self, app_model, message_id): message_id = str(message_id) diff --git a/api/controllers/console/app/statistic.py b/api/controllers/console/app/statistic.py index e3a5112200..51fe53c0ec 100644 --- a/api/controllers/console/app/statistic.py +++ b/api/controllers/console/app/statistic.py @@ -203,7 +203,7 @@ class AverageSessionInteractionStatistic(Resource): @setup_required @login_required @account_initialization_required - @get_app_model(mode=AppMode.CHAT) + @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT]) def get(self, app_model): account = current_user diff --git a/api/controllers/console/explore/completion.py b/api/controllers/console/explore/completion.py index 22ea4bbac2..dd531974fa 100644 --- a/api/controllers/console/explore/completion.py +++ b/api/controllers/console/explore/completion.py @@ -22,7 +22,7 @@ from controllers.console.app.error import ( from controllers.console.explore.error import NotChatAppError, NotCompletionAppError from controllers.console.explore.wraps import InstalledAppResource from core.app.app_queue_manager import AppQueueManager -from core.entities.application_entities import InvokeFrom +from core.app.entities.app_invoke_entities import InvokeFrom from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError from core.model_runtime.errors.invoke import InvokeError from extensions.ext_database import db diff --git a/api/controllers/console/explore/message.py b/api/controllers/console/explore/message.py index 47af28425f..fdb0eae24f 100644 --- a/api/controllers/console/explore/message.py +++ b/api/controllers/console/explore/message.py @@ -24,7 +24,7 @@ from controllers.console.explore.error import ( NotCompletionAppError, ) from controllers.console.explore.wraps import InstalledAppResource -from core.entities.application_entities import InvokeFrom +from core.app.entities.app_invoke_entities import InvokeFrom from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError from core.model_runtime.errors.invoke import InvokeError from fields.message_fields import message_infinite_scroll_pagination_fields diff --git a/api/controllers/service_api/app/completion.py b/api/controllers/service_api/app/completion.py index fd4ce831b3..5c488093fa 100644 --- a/api/controllers/service_api/app/completion.py +++ b/api/controllers/service_api/app/completion.py @@ -20,7 +20,7 @@ from controllers.service_api.app.error import ( ) from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token from core.app.app_queue_manager import AppQueueManager -from core.entities.application_entities import InvokeFrom +from core.app.entities.app_invoke_entities import InvokeFrom from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError from core.model_runtime.errors.invoke import InvokeError from libs.helper import uuid_value diff --git a/api/controllers/web/completion.py b/api/controllers/web/completion.py index fd94ec7646..785e2b8d6b 100644 --- a/api/controllers/web/completion.py +++ b/api/controllers/web/completion.py @@ -21,7 +21,7 @@ from controllers.web.error import ( ) from controllers.web.wraps import WebApiResource from core.app.app_queue_manager import AppQueueManager -from core.entities.application_entities import InvokeFrom +from core.app.entities.app_invoke_entities import InvokeFrom from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError from core.model_runtime.errors.invoke import InvokeError from libs.helper import uuid_value diff --git a/api/controllers/web/message.py b/api/controllers/web/message.py index e03bdd63bb..1acb92dbf1 100644 --- a/api/controllers/web/message.py +++ b/api/controllers/web/message.py @@ -21,7 +21,7 @@ from controllers.web.error import ( ProviderQuotaExceededError, ) from controllers.web.wraps import WebApiResource -from core.entities.application_entities import InvokeFrom +from core.app.entities.app_invoke_entities import InvokeFrom from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError from core.model_runtime.errors.invoke import InvokeError from fields.conversation_fields import message_file_fields diff --git a/api/core/agent/base_agent_runner.py b/api/core/agent/base_agent_runner.py index 1474c6a475..529240aecb 100644 --- a/api/core/agent/base_agent_runner.py +++ b/api/core/agent/base_agent_runner.py @@ -5,17 +5,15 @@ from datetime import datetime from mimetypes import guess_extension from typing import Optional, Union, cast +from core.agent.entities import AgentEntity, AgentToolEntity from core.app.app_queue_manager import AppQueueManager -from core.app.base_app_runner import AppRunner +from core.app.apps.agent_chat.app_config_manager import AgentChatAppConfig +from core.app.apps.base_app_runner import AppRunner from core.callback_handler.agent_tool_callback_handler import DifyAgentCallbackHandler from core.callback_handler.index_tool_callback_handler import DatasetIndexToolCallbackHandler -from core.entities.application_entities import ( - AgentEntity, - AgentToolEntity, - ApplicationGenerateEntity, - AppOrchestrationConfigEntity, - InvokeFrom, - ModelConfigEntity, +from core.app.entities.app_invoke_entities import ( + EasyUIBasedAppGenerateEntity, + InvokeFrom, EasyUIBasedModelConfigEntity, ) from core.file.message_file_parser import FileTransferMethod from core.memory.token_buffer_memory import TokenBufferMemory @@ -50,9 +48,9 @@ logger = logging.getLogger(__name__) class BaseAgentRunner(AppRunner): def __init__(self, tenant_id: str, - application_generate_entity: ApplicationGenerateEntity, - app_orchestration_config: AppOrchestrationConfigEntity, - model_config: ModelConfigEntity, + application_generate_entity: EasyUIBasedAppGenerateEntity, + app_config: AgentChatAppConfig, + model_config: EasyUIBasedModelConfigEntity, config: AgentEntity, queue_manager: AppQueueManager, message: Message, @@ -66,7 +64,7 @@ class BaseAgentRunner(AppRunner): """ Agent runner :param tenant_id: tenant id - :param app_orchestration_config: app orchestration config + :param app_config: app generate entity :param model_config: model config :param config: dataset config :param queue_manager: queue manager @@ -78,7 +76,7 @@ class BaseAgentRunner(AppRunner): """ self.tenant_id = tenant_id self.application_generate_entity = application_generate_entity - self.app_orchestration_config = app_orchestration_config + self.app_config = app_config self.model_config = model_config self.config = config self.queue_manager = queue_manager @@ -97,16 +95,16 @@ class BaseAgentRunner(AppRunner): # init dataset tools hit_callback = DatasetIndexToolCallbackHandler( queue_manager=queue_manager, - app_id=self.application_generate_entity.app_id, + app_id=self.app_config.app_id, message_id=message.id, user_id=user_id, invoke_from=self.application_generate_entity.invoke_from, ) self.dataset_tools = DatasetRetrieverTool.get_dataset_tools( tenant_id=tenant_id, - dataset_ids=app_orchestration_config.dataset.dataset_ids if app_orchestration_config.dataset else [], - retrieve_config=app_orchestration_config.dataset.retrieve_config if app_orchestration_config.dataset else None, - return_resource=app_orchestration_config.show_retrieve_source, + dataset_ids=app_config.dataset.dataset_ids if app_config.dataset else [], + retrieve_config=app_config.dataset.retrieve_config if app_config.dataset else None, + return_resource=app_config.additional_features.show_retrieve_source, invoke_from=application_generate_entity.invoke_from, hit_callback=hit_callback ) @@ -124,14 +122,15 @@ class BaseAgentRunner(AppRunner): else: self.stream_tool_call = False - def _repack_app_orchestration_config(self, app_orchestration_config: AppOrchestrationConfigEntity) -> AppOrchestrationConfigEntity: + def _repack_app_generate_entity(self, app_generate_entity: EasyUIBasedAppGenerateEntity) \ + -> EasyUIBasedAppGenerateEntity: """ - Repack app orchestration config + Repack app generate entity """ - if app_orchestration_config.prompt_template.simple_prompt_template is None: - app_orchestration_config.prompt_template.simple_prompt_template = '' + if app_generate_entity.app_config.prompt_template.simple_prompt_template is None: + app_generate_entity.app_config.prompt_template.simple_prompt_template = '' - return app_orchestration_config + return app_generate_entity def _convert_tool_response_to_str(self, tool_response: list[ToolInvokeMessage]) -> str: """ @@ -351,7 +350,7 @@ class BaseAgentRunner(AppRunner): )) db.session.close() - + return result def create_agent_thought(self, message_id: str, message: str, @@ -462,7 +461,7 @@ class BaseAgentRunner(AppRunner): db.session.commit() db.session.close() - + def transform_tool_invoke_messages(self, messages: list[ToolInvokeMessage]) -> list[ToolInvokeMessage]: """ Transform tool message into agent thought diff --git a/api/core/agent/cot_agent_runner.py b/api/core/agent/cot_agent_runner.py index 5650113f47..5b345f4da0 100644 --- a/api/core/agent/cot_agent_runner.py +++ b/api/core/agent/cot_agent_runner.py @@ -5,7 +5,7 @@ from typing import Literal, Union from core.agent.base_agent_runner import BaseAgentRunner from core.app.app_queue_manager import PublishFrom -from core.entities.application_entities import AgentPromptEntity, AgentScratchpadUnit +from core.agent.entities import AgentPromptEntity, AgentScratchpadUnit from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsage from core.model_runtime.entities.message_entities import ( AssistantPromptMessage, @@ -27,7 +27,7 @@ from core.tools.errors import ( from models.model import Conversation, Message -class AssistantCotApplicationRunner(BaseAssistantApplicationRunner): +class CotAgentRunner(BaseAgentRunner): _is_first_iteration = True _ignore_observation_providers = ['wenxin'] @@ -39,30 +39,33 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner): """ Run Cot agent application """ - app_orchestration_config = self.app_orchestration_config - self._repack_app_orchestration_config(app_orchestration_config) + app_generate_entity = self.application_generate_entity + self._repack_app_generate_entity(app_generate_entity) agent_scratchpad: list[AgentScratchpadUnit] = [] self._init_agent_scratchpad(agent_scratchpad, self.history_prompt_messages) - if 'Observation' not in app_orchestration_config.model_config.stop: - if app_orchestration_config.model_config.provider not in self._ignore_observation_providers: - app_orchestration_config.model_config.stop.append('Observation') + # check model mode + if 'Observation' not in app_generate_entity.model_config.stop: + if app_generate_entity.model_config.provider not in self._ignore_observation_providers: + app_generate_entity.model_config.stop.append('Observation') + + app_config = self.app_config # override inputs inputs = inputs or {} - instruction = self.app_orchestration_config.prompt_template.simple_prompt_template + instruction = app_config.prompt_template.simple_prompt_template instruction = self._fill_in_inputs_from_external_data_tools(instruction, inputs) iteration_step = 1 - max_iteration_steps = min(self.app_orchestration_config.agent.max_iteration, 5) + 1 + max_iteration_steps = min(app_config.agent.max_iteration, 5) + 1 prompt_messages = self.history_prompt_messages # convert tools into ModelRuntime Tool format prompt_messages_tools: list[PromptMessageTool] = [] tool_instances = {} - for tool in self.app_orchestration_config.agent.tools if self.app_orchestration_config.agent else []: + for tool in app_config.agent.tools if app_config.agent else []: try: prompt_tool, tool_entity = self._convert_tool_to_prompt_message_tool(tool) except Exception: @@ -122,11 +125,11 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner): # update prompt messages prompt_messages = self._organize_cot_prompt_messages( - mode=app_orchestration_config.model_config.mode, + mode=app_generate_entity.model_config.mode, prompt_messages=prompt_messages, tools=prompt_messages_tools, agent_scratchpad=agent_scratchpad, - agent_prompt_message=app_orchestration_config.agent.prompt, + agent_prompt_message=app_config.agent.prompt, instruction=instruction, input=query ) @@ -136,9 +139,9 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner): # invoke model chunks: Generator[LLMResultChunk, None, None] = model_instance.invoke_llm( prompt_messages=prompt_messages, - model_parameters=app_orchestration_config.model_config.parameters, + model_parameters=app_generate_entity.model_config.parameters, tools=[], - stop=app_orchestration_config.model_config.stop, + stop=app_generate_entity.model_config.stop, stream=True, user=self.user_id, callbacks=[], @@ -550,7 +553,7 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner): """ convert agent scratchpad list to str """ - next_iteration = self.app_orchestration_config.agent.prompt.next_iteration + next_iteration = self.app_config.agent.prompt.next_iteration result = '' for scratchpad in agent_scratchpad: diff --git a/api/core/agent/entities.py b/api/core/agent/entities.py new file mode 100644 index 0000000000..0fbfdc2636 --- /dev/null +++ b/api/core/agent/entities.py @@ -0,0 +1,61 @@ +from enum import Enum +from typing import Literal, Any, Union, Optional + +from pydantic import BaseModel + + +class AgentToolEntity(BaseModel): + """ + Agent Tool Entity. + """ + provider_type: Literal["builtin", "api"] + provider_id: str + tool_name: str + tool_parameters: dict[str, Any] = {} + + +class AgentPromptEntity(BaseModel): + """ + Agent Prompt Entity. + """ + first_prompt: str + next_iteration: str + + +class AgentScratchpadUnit(BaseModel): + """ + Agent First Prompt Entity. + """ + + class Action(BaseModel): + """ + Action Entity. + """ + action_name: str + action_input: Union[dict, str] + + agent_response: Optional[str] = None + thought: Optional[str] = None + action_str: Optional[str] = None + observation: Optional[str] = None + action: Optional[Action] = None + + +class AgentEntity(BaseModel): + """ + Agent Entity. + """ + + class Strategy(Enum): + """ + Agent Strategy. + """ + CHAIN_OF_THOUGHT = 'chain-of-thought' + FUNCTION_CALLING = 'function-calling' + + provider: str + model: str + strategy: Strategy + prompt: Optional[AgentPromptEntity] = None + tools: list[AgentToolEntity] = None + max_iteration: int = 5 diff --git a/api/core/agent/fc_agent_runner.py b/api/core/agent/fc_agent_runner.py index 9b238bf232..30e5cdd694 100644 --- a/api/core/agent/fc_agent_runner.py +++ b/api/core/agent/fc_agent_runner.py @@ -34,9 +34,11 @@ class FunctionCallAgentRunner(BaseAgentRunner): """ Run FunctionCall agent application """ - app_orchestration_config = self.app_orchestration_config + app_generate_entity = self.application_generate_entity - prompt_template = self.app_orchestration_config.prompt_template.simple_prompt_template or '' + app_config = self.app_config + + prompt_template = app_config.prompt_template.simple_prompt_template or '' prompt_messages = self.history_prompt_messages prompt_messages = self.organize_prompt_messages( prompt_template=prompt_template, @@ -47,7 +49,7 @@ class FunctionCallAgentRunner(BaseAgentRunner): # convert tools into ModelRuntime Tool format prompt_messages_tools: list[PromptMessageTool] = [] tool_instances = {} - for tool in self.app_orchestration_config.agent.tools if self.app_orchestration_config.agent else []: + for tool in app_config.agent.tools if app_config.agent else []: try: prompt_tool, tool_entity = self._convert_tool_to_prompt_message_tool(tool) except Exception: @@ -67,7 +69,7 @@ class FunctionCallAgentRunner(BaseAgentRunner): tool_instances[dataset_tool.identity.name] = dataset_tool iteration_step = 1 - max_iteration_steps = min(app_orchestration_config.agent.max_iteration, 5) + 1 + max_iteration_steps = min(app_config.agent.max_iteration, 5) + 1 # continue to run until there is not any tool call function_call_state = True @@ -110,9 +112,9 @@ class FunctionCallAgentRunner(BaseAgentRunner): # invoke model chunks: Union[Generator[LLMResultChunk, None, None], LLMResult] = model_instance.invoke_llm( prompt_messages=prompt_messages, - model_parameters=app_orchestration_config.model_config.parameters, + model_parameters=app_generate_entity.model_config.parameters, tools=prompt_messages_tools, - stop=app_orchestration_config.model_config.stop, + stop=app_generate_entity.model_config.stop, stream=self.stream_tool_call, user=self.user_id, callbacks=[], diff --git a/api/core/app/advanced_chat/config_validator.py b/api/core/app/advanced_chat/config_validator.py deleted file mode 100644 index a20198ef4a..0000000000 --- a/api/core/app/advanced_chat/config_validator.py +++ /dev/null @@ -1,59 +0,0 @@ -from core.app.validators.file_upload import FileUploadValidator -from core.app.validators.moderation import ModerationValidator -from core.app.validators.opening_statement import OpeningStatementValidator -from core.app.validators.retriever_resource import RetrieverResourceValidator -from core.app.validators.speech_to_text import SpeechToTextValidator -from core.app.validators.suggested_questions import SuggestedQuestionsValidator -from core.app.validators.text_to_speech import TextToSpeechValidator - - -class AdvancedChatAppConfigValidator: - @classmethod - def config_validate(cls, tenant_id: str, config: dict, only_structure_validate: bool = False) -> dict: - """ - Validate for advanced chat app model config - - :param tenant_id: tenant id - :param config: app model config args - :param only_structure_validate: if True, only structure validation will be performed - """ - 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=tenant_id, - config=config, - only_structure_validate=only_structure_validate - ) - 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/app/advanced_chat/__init__.py b/api/core/app/app_config/__init__.py similarity index 100% rename from api/core/app/advanced_chat/__init__.py rename to api/core/app/app_config/__init__.py diff --git a/api/core/app/app_config/base_app_config_manager.py b/api/core/app/app_config/base_app_config_manager.py new file mode 100644 index 0000000000..b3c773203d --- /dev/null +++ b/api/core/app/app_config/base_app_config_manager.py @@ -0,0 +1,73 @@ +from typing import Union, Optional + +from core.app.app_config.entities import AppAdditionalFeatures, EasyUIBasedAppModelConfigFrom +from core.app.app_config.features.file_upload.manager import FileUploadConfigManager +from core.app.app_config.features.more_like_this.manager import MoreLikeThisConfigManager +from core.app.app_config.features.opening_statement.manager import OpeningStatementConfigManager +from core.app.app_config.features.retrieval_resource.manager import RetrievalResourceConfigManager +from core.app.app_config.features.speech_to_text.manager import SpeechToTextConfigManager +from core.app.app_config.features.suggested_questions_after_answer.manager import \ + SuggestedQuestionsAfterAnswerConfigManager +from core.app.app_config.features.text_to_speech.manager import TextToSpeechConfigManager +from models.model import AppModelConfig + + +class BaseAppConfigManager: + + @classmethod + def convert_to_config_dict(cls, config_from: EasyUIBasedAppModelConfigFrom, + app_model_config: Union[AppModelConfig, dict], + config_dict: Optional[dict] = None) -> dict: + """ + Convert app model config to config dict + :param config_from: app model config from + :param app_model_config: app model config + :param config_dict: app model config dict + :return: + """ + if config_from != EasyUIBasedAppModelConfigFrom.ARGS: + app_model_config_dict = app_model_config.to_dict() + config_dict = app_model_config_dict.copy() + + return config_dict + + @classmethod + def convert_features(cls, config_dict: dict) -> AppAdditionalFeatures: + """ + Convert app config to app model config + + :param config_dict: app config + """ + config_dict = config_dict.copy() + + additional_features = AppAdditionalFeatures() + additional_features.show_retrieve_source = RetrievalResourceConfigManager.convert( + config=config_dict + ) + + additional_features.file_upload = FileUploadConfigManager.convert( + config=config_dict + ) + + additional_features.opening_statement, additional_features.suggested_questions = \ + OpeningStatementConfigManager.convert( + config=config_dict + ) + + additional_features.suggested_questions_after_answer = SuggestedQuestionsAfterAnswerConfigManager.convert( + config=config_dict + ) + + additional_features.more_like_this = MoreLikeThisConfigManager.convert( + config=config_dict + ) + + additional_features.speech_to_text = SpeechToTextConfigManager.convert( + config=config_dict + ) + + additional_features.text_to_speech = TextToSpeechConfigManager.convert( + config=config_dict + ) + + return additional_features diff --git a/api/core/app/agent_chat/__init__.py b/api/core/app/app_config/common/__init__.py similarity index 100% rename from api/core/app/agent_chat/__init__.py rename to api/core/app/app_config/common/__init__.py diff --git a/api/core/app/chat/__init__.py b/api/core/app/app_config/common/sensitive_word_avoidance/__init__.py similarity index 100% rename from api/core/app/chat/__init__.py rename to api/core/app/app_config/common/sensitive_word_avoidance/__init__.py diff --git a/api/core/app/validators/moderation.py b/api/core/app/app_config/common/sensitive_word_avoidance/manager.py similarity index 64% rename from api/core/app/validators/moderation.py rename to api/core/app/app_config/common/sensitive_word_avoidance/manager.py index 7a5dff55c9..3dccfa3cbe 100644 --- a/api/core/app/validators/moderation.py +++ b/api/core/app/app_config/common/sensitive_word_avoidance/manager.py @@ -1,11 +1,24 @@ -import logging +from typing import Optional +from core.app.app_config.entities import SensitiveWordAvoidanceEntity from core.moderation.factory import ModerationFactory -logger = logging.getLogger(__name__) +class SensitiveWordAvoidanceConfigManager: + @classmethod + def convert(cls, config: dict) -> Optional[SensitiveWordAvoidanceEntity]: + sensitive_word_avoidance_dict = config.get('sensitive_word_avoidance') + if not sensitive_word_avoidance_dict: + return None + + if 'enabled' in sensitive_word_avoidance_dict and sensitive_word_avoidance_dict['enabled']: + return SensitiveWordAvoidanceEntity( + type=sensitive_word_avoidance_dict.get('type'), + config=sensitive_word_avoidance_dict.get('config'), + ) + else: + return None -class ModerationValidator: @classmethod def validate_and_set_defaults(cls, tenant_id, config: dict, only_structure_validate: bool = False) \ -> tuple[dict, list[str]]: diff --git a/api/core/app/completion/__init__.py b/api/core/app/app_config/easy_ui_based_app/__init__.py similarity index 100% rename from api/core/app/completion/__init__.py rename to api/core/app/app_config/easy_ui_based_app/__init__.py diff --git a/api/core/app/validators/__init__.py b/api/core/app/app_config/easy_ui_based_app/agent/__init__.py similarity index 100% rename from api/core/app/validators/__init__.py rename to api/core/app/app_config/easy_ui_based_app/agent/__init__.py diff --git a/api/core/app/app_config/easy_ui_based_app/agent/manager.py b/api/core/app/app_config/easy_ui_based_app/agent/manager.py new file mode 100644 index 0000000000..b50b7f678c --- /dev/null +++ b/api/core/app/app_config/easy_ui_based_app/agent/manager.py @@ -0,0 +1,79 @@ +from typing import Optional + +from core.agent.entities import AgentEntity, AgentPromptEntity, AgentToolEntity +from core.tools.prompt.template import REACT_PROMPT_TEMPLATES + + +class AgentConfigManager: + @classmethod + def convert(cls, config: dict) -> Optional[AgentEntity]: + """ + Convert model config to model config + + :param config: model config args + """ + if 'agent_mode' in config and config['agent_mode'] \ + and 'enabled' in config['agent_mode'] \ + and config['agent_mode']['enabled']: + + agent_dict = config.get('agent_mode', {}) + agent_strategy = agent_dict.get('strategy', 'cot') + + if agent_strategy == 'function_call': + strategy = AgentEntity.Strategy.FUNCTION_CALLING + elif agent_strategy == 'cot' or agent_strategy == 'react': + strategy = AgentEntity.Strategy.CHAIN_OF_THOUGHT + else: + # old configs, try to detect default strategy + if config['model']['provider'] == 'openai': + strategy = AgentEntity.Strategy.FUNCTION_CALLING + else: + strategy = AgentEntity.Strategy.CHAIN_OF_THOUGHT + + agent_tools = [] + for tool in agent_dict.get('tools', []): + keys = tool.keys() + if len(keys) >= 4: + if "enabled" not in tool or not tool["enabled"]: + continue + + agent_tool_properties = { + 'provider_type': tool['provider_type'], + 'provider_id': tool['provider_id'], + 'tool_name': tool['tool_name'], + 'tool_parameters': tool['tool_parameters'] if 'tool_parameters' in tool else {} + } + + agent_tools.append(AgentToolEntity(**agent_tool_properties)) + + if 'strategy' in config['agent_mode'] and \ + config['agent_mode']['strategy'] not in ['react_router', 'router']: + agent_prompt = agent_dict.get('prompt', None) or {} + # check model mode + model_mode = config.get('model', {}).get('mode', 'completion') + if model_mode == 'completion': + agent_prompt_entity = AgentPromptEntity( + first_prompt=agent_prompt.get('first_prompt', + REACT_PROMPT_TEMPLATES['english']['completion']['prompt']), + next_iteration=agent_prompt.get('next_iteration', + REACT_PROMPT_TEMPLATES['english']['completion'][ + 'agent_scratchpad']), + ) + else: + agent_prompt_entity = AgentPromptEntity( + first_prompt=agent_prompt.get('first_prompt', + REACT_PROMPT_TEMPLATES['english']['chat']['prompt']), + next_iteration=agent_prompt.get('next_iteration', + REACT_PROMPT_TEMPLATES['english']['chat']['agent_scratchpad']), + ) + + return AgentEntity( + provider=config['model']['provider'], + model=config['model']['name'], + strategy=strategy, + prompt=agent_prompt_entity, + tools=agent_tools, + max_iteration=agent_dict.get('max_iteration', 5) + ) + + return None diff --git a/api/core/app/workflow/__init__.py b/api/core/app/app_config/easy_ui_based_app/dataset/__init__.py similarity index 100% rename from api/core/app/workflow/__init__.py rename to api/core/app/app_config/easy_ui_based_app/dataset/__init__.py diff --git a/api/core/app/validators/dataset_retrieval.py b/api/core/app/app_config/easy_ui_based_app/dataset/manager.py similarity index 63% rename from api/core/app/validators/dataset_retrieval.py rename to api/core/app/app_config/easy_ui_based_app/dataset/manager.py index fb5b648320..4c08f62d27 100644 --- a/api/core/app/validators/dataset_retrieval.py +++ b/api/core/app/app_config/easy_ui_based_app/dataset/manager.py @@ -1,11 +1,94 @@ -import uuid +from typing import Optional +from core.app.app_config.entities import DatasetEntity, DatasetRetrieveConfigEntity from core.entities.agent_entities import PlanningStrategy from models.model import AppMode from services.dataset_service import DatasetService -class DatasetValidator: +class DatasetConfigManager: + @classmethod + def convert(cls, config: dict) -> Optional[DatasetEntity]: + """ + Convert model config to model config + + :param config: model config args + """ + dataset_ids = [] + if 'datasets' in config.get('dataset_configs', {}): + datasets = config.get('dataset_configs', {}).get('datasets', { + 'strategy': 'router', + 'datasets': [] + }) + + for dataset in datasets.get('datasets', []): + keys = list(dataset.keys()) + if len(keys) == 0 or keys[0] != 'dataset': + continue + + dataset = dataset['dataset'] + + if 'enabled' not in dataset or not dataset['enabled']: + continue + + dataset_id = dataset.get('id', None) + if dataset_id: + dataset_ids.append(dataset_id) + + if 'agent_mode' in config and config['agent_mode'] \ + and 'enabled' in config['agent_mode'] \ + and config['agent_mode']['enabled']: + + agent_dict = config.get('agent_mode', {}) + + for tool in agent_dict.get('tools', []): + keys = tool.keys() + if len(keys) == 1: + # old standard + key = list(tool.keys())[0] + + if key != 'dataset': + continue + + tool_item = tool[key] + + if "enabled" not in tool_item or not tool_item["enabled"]: + continue + + dataset_id = tool_item['id'] + dataset_ids.append(dataset_id) + + if len(dataset_ids) == 0: + return None + + # dataset configs + dataset_configs = config.get('dataset_configs', {'retrieval_model': 'single'}) + query_variable = config.get('dataset_query_variable') + + if dataset_configs['retrieval_model'] == 'single': + return DatasetEntity( + dataset_ids=dataset_ids, + retrieve_config=DatasetRetrieveConfigEntity( + query_variable=query_variable, + retrieve_strategy=DatasetRetrieveConfigEntity.RetrieveStrategy.value_of( + dataset_configs['retrieval_model'] + ) + ) + ) + else: + return DatasetEntity( + dataset_ids=dataset_ids, + retrieve_config=DatasetRetrieveConfigEntity( + query_variable=query_variable, + retrieve_strategy=DatasetRetrieveConfigEntity.RetrieveStrategy.value_of( + dataset_configs['retrieval_model'] + ), + top_k=dataset_configs.get('top_k'), + score_threshold=dataset_configs.get('score_threshold'), + reranking_model=dataset_configs.get('reranking_model') + ) + ) + @classmethod def validate_and_set_defaults(cls, tenant_id: str, app_mode: AppMode, config: dict) -> tuple[dict, list[str]]: """ diff --git a/api/core/app/app_config/easy_ui_based_app/model_config/__init__.py b/api/core/app/app_config/easy_ui_based_app/model_config/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/app/app_config/easy_ui_based_app/model_config/converter.py b/api/core/app/app_config/easy_ui_based_app/model_config/converter.py new file mode 100644 index 0000000000..05fcb10791 --- /dev/null +++ b/api/core/app/app_config/easy_ui_based_app/model_config/converter.py @@ -0,0 +1,104 @@ +from typing import cast + +from core.app.app_config.entities import EasyUIBasedAppConfig +from core.app.entities.app_invoke_entities import EasyUIBasedModelConfigEntity + +from core.entities.model_entities import ModelStatus +from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError +from core.model_runtime.entities.model_entities import ModelType +from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel +from core.provider_manager import ProviderManager + + +class EasyUIBasedModelConfigEntityConverter: + @classmethod + def convert(cls, app_config: EasyUIBasedAppConfig, + skip_check: bool = False) \ + -> EasyUIBasedModelConfigEntity: + """ + Convert app model config dict to entity. + :param app_config: app config + :param skip_check: skip check + :raises ProviderTokenNotInitError: provider token not init error + :return: app orchestration config entity + """ + model_config = app_config.model + + provider_manager = ProviderManager() + provider_model_bundle = provider_manager.get_provider_model_bundle( + tenant_id=app_config.tenant_id, + provider=model_config.provider, + model_type=ModelType.LLM + ) + + provider_name = provider_model_bundle.configuration.provider.provider + model_name = model_config.model + + model_type_instance = provider_model_bundle.model_type_instance + model_type_instance = cast(LargeLanguageModel, model_type_instance) + + # check model credentials + model_credentials = provider_model_bundle.configuration.get_current_credentials( + model_type=ModelType.LLM, + model=model_config.model + ) + + if model_credentials is None: + if not skip_check: + raise ProviderTokenNotInitError(f"Model {model_name} credentials is not initialized.") + else: + model_credentials = {} + + if not skip_check: + # check model + provider_model = provider_model_bundle.configuration.get_provider_model( + model=model_config.model, + model_type=ModelType.LLM + ) + + if provider_model is None: + model_name = model_config.model + raise ValueError(f"Model {model_name} not exist.") + + if provider_model.status == ModelStatus.NO_CONFIGURE: + raise ProviderTokenNotInitError(f"Model {model_name} credentials is not initialized.") + elif provider_model.status == ModelStatus.NO_PERMISSION: + raise ModelCurrentlyNotSupportError(f"Dify Hosted OpenAI {model_name} currently not support.") + elif provider_model.status == ModelStatus.QUOTA_EXCEEDED: + raise QuotaExceededError(f"Model provider {provider_name} quota exceeded.") + + # model config + completion_params = model_config.parameters + stop = [] + if 'stop' in completion_params: + stop = completion_params['stop'] + del completion_params['stop'] + + # get model mode + model_mode = model_config.mode + if not model_mode: + mode_enum = model_type_instance.get_model_mode( + model=model_config.model, + credentials=model_credentials + ) + + model_mode = mode_enum.value + + model_schema = model_type_instance.get_model_schema( + model_config.model, + model_credentials + ) + + if not skip_check and not model_schema: + raise ValueError(f"Model {model_name} not exist.") + + return EasyUIBasedModelConfigEntity( + provider=model_config.provider, + model=model_config.model, + model_schema=model_schema, + mode=model_mode, + provider_model_bundle=provider_model_bundle, + credentials=model_credentials, + parameters=completion_params, + stop=stop, + ) diff --git a/api/core/app/validators/model_validator.py b/api/core/app/app_config/easy_ui_based_app/model_config/manager.py similarity index 73% rename from api/core/app/validators/model_validator.py rename to api/core/app/app_config/easy_ui_based_app/model_config/manager.py index 1d86fbaf04..5cca2bc1a7 100644 --- a/api/core/app/validators/model_validator.py +++ b/api/core/app/app_config/easy_ui_based_app/model_config/manager.py @@ -1,10 +1,40 @@ - -from core.model_runtime.entities.model_entities import ModelPropertyKey, ModelType +from core.app.app_config.entities import ModelConfigEntity +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: +class ModelConfigManager: + @classmethod + def convert(cls, config: dict) -> ModelConfigEntity: + """ + Convert model config to model config + + :param config: model config args + """ + # model config + model_config = config.get('model') + + if not model_config: + raise ValueError("model is required") + + completion_params = model_config.get('completion_params') + stop = [] + if 'stop' in completion_params: + stop = completion_params['stop'] + del completion_params['stop'] + + # get model mode + model_mode = model_config.get('mode') + + return ModelConfigEntity( + provider=config['model']['provider'], + model=config['model']['name'], + mode=model_mode, + parameters=completion_params, + stop=stop, + ) + @classmethod def validate_and_set_defaults(cls, tenant_id: str, config: dict) -> tuple[dict, list[str]]: """ diff --git a/api/core/app/app_config/easy_ui_based_app/prompt_template/__init__.py b/api/core/app/app_config/easy_ui_based_app/prompt_template/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/app/validators/prompt.py b/api/core/app/app_config/easy_ui_based_app/prompt_template/manager.py similarity index 58% rename from api/core/app/validators/prompt.py rename to api/core/app/app_config/easy_ui_based_app/prompt_template/manager.py index 288a523415..5629d0d09e 100644 --- a/api/core/app/validators/prompt.py +++ b/api/core/app/app_config/easy_ui_based_app/prompt_template/manager.py @@ -1,10 +1,61 @@ - -from core.entities.application_entities import PromptTemplateEntity +from core.app.app_config.entities import PromptTemplateEntity, \ + AdvancedChatPromptTemplateEntity, AdvancedCompletionPromptTemplateEntity +from core.model_runtime.entities.message_entities import PromptMessageRole from core.prompt.simple_prompt_transform import ModelMode from models.model import AppMode -class PromptValidator: +class PromptTemplateConfigManager: + @classmethod + def convert(cls, config: dict) -> PromptTemplateEntity: + if not config.get("prompt_type"): + raise ValueError("prompt_type is required") + + prompt_type = PromptTemplateEntity.PromptType.value_of(config['prompt_type']) + if prompt_type == PromptTemplateEntity.PromptType.SIMPLE: + simple_prompt_template = config.get("pre_prompt", "") + return PromptTemplateEntity( + prompt_type=prompt_type, + simple_prompt_template=simple_prompt_template + ) + else: + advanced_chat_prompt_template = None + chat_prompt_config = config.get("chat_prompt_config", {}) + if chat_prompt_config: + chat_prompt_messages = [] + for message in chat_prompt_config.get("prompt", []): + chat_prompt_messages.append({ + "text": message["text"], + "role": PromptMessageRole.value_of(message["role"]) + }) + + advanced_chat_prompt_template = AdvancedChatPromptTemplateEntity( + messages=chat_prompt_messages + ) + + advanced_completion_prompt_template = None + completion_prompt_config = config.get("completion_prompt_config", {}) + if completion_prompt_config: + completion_prompt_template_params = { + 'prompt': completion_prompt_config['prompt']['text'], + } + + if 'conversation_histories_role' in completion_prompt_config: + completion_prompt_template_params['role_prefix'] = { + 'user': completion_prompt_config['conversation_histories_role']['user_prefix'], + 'assistant': completion_prompt_config['conversation_histories_role']['assistant_prefix'] + } + + advanced_completion_prompt_template = AdvancedCompletionPromptTemplateEntity( + **completion_prompt_template_params + ) + + return PromptTemplateEntity( + prompt_type=prompt_type, + advanced_chat_prompt_template=advanced_chat_prompt_template, + advanced_completion_prompt_template=advanced_completion_prompt_template + ) + @classmethod def validate_and_set_defaults(cls, app_mode: AppMode, config: dict) -> tuple[dict, list[str]]: """ @@ -83,4 +134,4 @@ class PromptValidator: if not isinstance(config["post_prompt"], str): raise ValueError("post_prompt must be of string type") - return config \ No newline at end of file + return config diff --git a/api/core/app/app_config/easy_ui_based_app/variables/__init__.py b/api/core/app/app_config/easy_ui_based_app/variables/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/app/app_config/easy_ui_based_app/variables/manager.py b/api/core/app/app_config/easy_ui_based_app/variables/manager.py new file mode 100644 index 0000000000..ff962a5439 --- /dev/null +++ b/api/core/app/app_config/easy_ui_based_app/variables/manager.py @@ -0,0 +1,184 @@ +import re +from typing import Tuple + +from core.app.app_config.entities import VariableEntity, ExternalDataVariableEntity +from core.external_data_tool.factory import ExternalDataToolFactory + + +class BasicVariablesConfigManager: + @classmethod + def convert(cls, config: dict) -> Tuple[list[VariableEntity], list[ExternalDataVariableEntity]]: + """ + Convert model config to model config + + :param config: model config args + """ + external_data_variables = [] + variables = [] + + # old external_data_tools + external_data_tools = config.get('external_data_tools', []) + for external_data_tool in external_data_tools: + if 'enabled' not in external_data_tool or not external_data_tool['enabled']: + continue + + external_data_variables.append( + ExternalDataVariableEntity( + variable=external_data_tool['variable'], + type=external_data_tool['type'], + config=external_data_tool['config'] + ) + ) + + # variables and external_data_tools + for variable in config.get('user_input_form', []): + typ = list(variable.keys())[0] + if typ == 'external_data_tool': + val = variable[typ] + external_data_variables.append( + ExternalDataVariableEntity( + variable=val['variable'], + type=val['type'], + config=val['config'] + ) + ) + elif typ in [ + VariableEntity.Type.TEXT_INPUT.value, + VariableEntity.Type.PARAGRAPH.value, + VariableEntity.Type.NUMBER.value, + ]: + variables.append( + VariableEntity( + type=VariableEntity.Type.value_of(typ), + variable=variable[typ].get('variable'), + description=variable[typ].get('description'), + label=variable[typ].get('label'), + required=variable[typ].get('required', False), + max_length=variable[typ].get('max_length'), + default=variable[typ].get('default'), + ) + ) + elif typ == VariableEntity.Type.SELECT.value: + variables.append( + VariableEntity( + type=VariableEntity.Type.SELECT, + variable=variable[typ].get('variable'), + description=variable[typ].get('description'), + label=variable[typ].get('label'), + required=variable[typ].get('required', False), + options=variable[typ].get('options'), + default=variable[typ].get('default'), + ) + ) + + return variables, external_data_variables + + @classmethod + def validate_and_set_defaults(cls, tenant_id: str, config: dict) -> tuple[dict, list[str]]: + """ + Validate and set defaults for user input form + + :param tenant_id: workspace id + :param config: app model config args + """ + related_config_keys = [] + config, current_related_config_keys = cls.validate_variables_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + config, current_related_config_keys = cls.validate_external_data_tools_and_set_defaults(tenant_id, config) + related_config_keys.extend(current_related_config_keys) + + return config, related_config_keys + + @classmethod + def validate_variables_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"] + + @classmethod + def validate_external_data_tools_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"] \ No newline at end of file diff --git a/api/core/entities/application_entities.py b/api/core/app/app_config/entities.py similarity index 61% rename from api/core/entities/application_entities.py rename to api/core/app/app_config/entities.py index f5ea4d1eb0..e155dc1c4d 100644 --- a/api/core/entities/application_entities.py +++ b/api/core/app/app_config/entities.py @@ -1,12 +1,10 @@ from enum import Enum -from typing import Any, Literal, Optional, Union +from typing import Any, Optional from pydantic import BaseModel -from core.entities.provider_configuration import ProviderModelBundle -from core.file.file_obj import FileObj from core.model_runtime.entities.message_entities import PromptMessageRole -from core.model_runtime.entities.model_entities import AIModelEntity +from models.model import AppMode class ModelConfigEntity(BaseModel): @@ -15,10 +13,7 @@ class ModelConfigEntity(BaseModel): """ provider: str model: str - model_schema: Optional[AIModelEntity] = None - mode: str - provider_model_bundle: ProviderModelBundle - credentials: dict[str, Any] = {} + mode: Optional[str] = None parameters: dict[str, Any] = {} stop: list[str] = [] @@ -194,149 +189,53 @@ class FileUploadEntity(BaseModel): image_config: Optional[dict[str, Any]] = None -class AgentToolEntity(BaseModel): - """ - Agent Tool Entity. - """ - provider_type: Literal["builtin", "api"] - provider_id: str - tool_name: str - tool_parameters: dict[str, Any] = {} - - -class AgentPromptEntity(BaseModel): - """ - Agent Prompt Entity. - """ - first_prompt: str - next_iteration: str - - -class AgentScratchpadUnit(BaseModel): - """ - Agent First Prompt Entity. - """ - - class Action(BaseModel): - """ - Action Entity. - """ - action_name: str - action_input: Union[dict, str] - - agent_response: Optional[str] = None - thought: Optional[str] = None - action_str: Optional[str] = None - observation: Optional[str] = None - action: Optional[Action] = None - - -class AgentEntity(BaseModel): - """ - Agent Entity. - """ - - class Strategy(Enum): - """ - Agent Strategy. - """ - CHAIN_OF_THOUGHT = 'chain-of-thought' - FUNCTION_CALLING = 'function-calling' - - provider: str - model: str - strategy: Strategy - prompt: Optional[AgentPromptEntity] = None - tools: list[AgentToolEntity] = None - max_iteration: int = 5 - - -class AppOrchestrationConfigEntity(BaseModel): - """ - App Orchestration Config Entity. - """ - model_config: ModelConfigEntity - prompt_template: PromptTemplateEntity - variables: list[VariableEntity] = [] - external_data_variables: list[ExternalDataVariableEntity] = [] - agent: Optional[AgentEntity] = None - - # features - dataset: Optional[DatasetEntity] = None +class AppAdditionalFeatures(BaseModel): file_upload: Optional[FileUploadEntity] = None opening_statement: Optional[str] = None + suggested_questions: list[str] = [] suggested_questions_after_answer: bool = False show_retrieve_source: bool = False more_like_this: bool = False speech_to_text: bool = False text_to_speech: Optional[TextToSpeechEntity] = None + + +class AppConfig(BaseModel): + """ + Application Config Entity. + """ + tenant_id: str + app_id: str + app_mode: AppMode + additional_features: AppAdditionalFeatures + variables: list[VariableEntity] = [] sensitive_word_avoidance: Optional[SensitiveWordAvoidanceEntity] = None -class InvokeFrom(Enum): +class EasyUIBasedAppModelConfigFrom(Enum): """ - Invoke From. + App Model Config From. """ - SERVICE_API = 'service-api' - WEB_APP = 'web-app' - EXPLORE = 'explore' - DEBUGGER = 'debugger' - - @classmethod - def value_of(cls, value: str) -> 'InvokeFrom': - """ - Get value of given mode. - - :param value: mode value - :return: mode - """ - for mode in cls: - if mode.value == value: - return mode - raise ValueError(f'invalid invoke from value {value}') - - def to_source(self) -> str: - """ - Get source of invoke from. - - :return: source - """ - if self == InvokeFrom.WEB_APP: - return 'web_app' - elif self == InvokeFrom.DEBUGGER: - return 'dev' - elif self == InvokeFrom.EXPLORE: - return 'explore_app' - elif self == InvokeFrom.SERVICE_API: - return 'api' - - return 'dev' + ARGS = 'args' + APP_LATEST_CONFIG = 'app-latest-config' + CONVERSATION_SPECIFIC_CONFIG = 'conversation-specific-config' -class ApplicationGenerateEntity(BaseModel): +class EasyUIBasedAppConfig(AppConfig): """ - Application Generate Entity. + Easy UI Based App Config Entity. """ - task_id: str - tenant_id: str - - app_id: str + app_model_config_from: EasyUIBasedAppModelConfigFrom app_model_config_id: str - # for save app_model_config_dict: dict - app_model_config_override: bool + model: ModelConfigEntity + prompt_template: PromptTemplateEntity + dataset: Optional[DatasetEntity] = None + external_data_variables: list[ExternalDataVariableEntity] = [] - # Converted from app_model_config to Entity object, or directly covered by external input - app_orchestration_config_entity: AppOrchestrationConfigEntity - conversation_id: Optional[str] = None - inputs: dict[str, str] - query: Optional[str] = None - files: list[FileObj] = [] - user_id: str - # extras - stream: bool - invoke_from: InvokeFrom - - # extra parameters, like: auto_generate_conversation_name - extras: dict[str, Any] = {} +class WorkflowUIBasedAppConfig(AppConfig): + """ + Workflow UI Based App Config Entity. + """ + workflow_id: str diff --git a/api/core/app/app_config/features/__init__.py b/api/core/app/app_config/features/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/app/app_config/features/file_upload/__init__.py b/api/core/app/app_config/features/file_upload/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/app/validators/file_upload.py b/api/core/app/app_config/features/file_upload/manager.py similarity index 59% rename from api/core/app/validators/file_upload.py rename to api/core/app/app_config/features/file_upload/manager.py index 419465bd51..63830696ff 100644 --- a/api/core/app/validators/file_upload.py +++ b/api/core/app/app_config/features/file_upload/manager.py @@ -1,6 +1,30 @@ +from typing import Optional + +from core.app.app_config.entities import FileUploadEntity -class FileUploadValidator: +class FileUploadConfigManager: + @classmethod + def convert(cls, config: dict) -> Optional[FileUploadEntity]: + """ + Convert model config to model config + + :param config: model config args + """ + file_upload_dict = config.get('file_upload') + if file_upload_dict: + if 'image' in file_upload_dict and file_upload_dict['image']: + if 'enabled' in file_upload_dict['image'] and file_upload_dict['image']['enabled']: + return FileUploadEntity( + image_config={ + 'number_limits': file_upload_dict['image']['number_limits'], + 'detail': file_upload_dict['image']['detail'], + 'transfer_methods': file_upload_dict['image']['transfer_methods'] + } + ) + + return None + @classmethod def validate_and_set_defaults(cls, config: dict) -> tuple[dict, list[str]]: """ diff --git a/api/core/app/app_config/features/more_like_this/__init__.py b/api/core/app/app_config/features/more_like_this/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/app/validators/more_like_this.py b/api/core/app/app_config/features/more_like_this/manager.py similarity index 63% rename from api/core/app/validators/more_like_this.py rename to api/core/app/app_config/features/more_like_this/manager.py index 1c1bac9de6..ec2a9a6796 100644 --- a/api/core/app/validators/more_like_this.py +++ b/api/core/app/app_config/features/more_like_this/manager.py @@ -1,6 +1,19 @@ +class MoreLikeThisConfigManager: + @classmethod + def convert(cls, config: dict) -> bool: + """ + Convert model config to model config + :param config: model config args + """ + more_like_this = False + more_like_this_dict = config.get('more_like_this') + if more_like_this_dict: + if 'enabled' in more_like_this_dict and more_like_this_dict['enabled']: + more_like_this = True + + return more_like_this -class MoreLikeThisValidator: @classmethod def validate_and_set_defaults(cls, config: dict) -> tuple[dict, list[str]]: """ diff --git a/api/core/app/app_config/features/opening_statement/__init__.py b/api/core/app/app_config/features/opening_statement/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/app/validators/opening_statement.py b/api/core/app/app_config/features/opening_statement/manager.py similarity index 66% rename from api/core/app/validators/opening_statement.py rename to api/core/app/app_config/features/opening_statement/manager.py index f919230e0d..6183c6e749 100644 --- a/api/core/app/validators/opening_statement.py +++ b/api/core/app/app_config/features/opening_statement/manager.py @@ -1,6 +1,22 @@ +from typing import Tuple -class OpeningStatementValidator: +class OpeningStatementConfigManager: + @classmethod + def convert(cls, config: dict) -> Tuple[str, list]: + """ + Convert model config to model config + + :param config: model config args + """ + # opening statement + opening_statement = config.get('opening_statement') + + # suggested questions + suggested_questions_list = config.get('suggested_questions') + + return opening_statement, suggested_questions_list + @classmethod def validate_and_set_defaults(cls, config: dict) -> tuple[dict, list[str]]: """ diff --git a/api/core/app/app_config/features/retrieval_resource/__init__.py b/api/core/app/app_config/features/retrieval_resource/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/app/validators/retriever_resource.py b/api/core/app/app_config/features/retrieval_resource/manager.py similarity index 68% rename from api/core/app/validators/retriever_resource.py rename to api/core/app/app_config/features/retrieval_resource/manager.py index 32725c7432..0694cb954e 100644 --- a/api/core/app/validators/retriever_resource.py +++ b/api/core/app/app_config/features/retrieval_resource/manager.py @@ -1,6 +1,14 @@ +class RetrievalResourceConfigManager: + @classmethod + def convert(cls, config: dict) -> bool: + show_retrieve_source = False + retriever_resource_dict = config.get('retriever_resource') + if retriever_resource_dict: + if 'enabled' in retriever_resource_dict and retriever_resource_dict['enabled']: + show_retrieve_source = True + return show_retrieve_source -class RetrieverResourceValidator: @classmethod def validate_and_set_defaults(cls, config: dict) -> tuple[dict, list[str]]: """ diff --git a/api/core/app/app_config/features/speech_to_text/__init__.py b/api/core/app/app_config/features/speech_to_text/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/app/validators/speech_to_text.py b/api/core/app/app_config/features/speech_to_text/manager.py similarity index 63% rename from api/core/app/validators/speech_to_text.py rename to api/core/app/app_config/features/speech_to_text/manager.py index 92a1b25ae6..b98699bfff 100644 --- a/api/core/app/validators/speech_to_text.py +++ b/api/core/app/app_config/features/speech_to_text/manager.py @@ -1,6 +1,19 @@ +class SpeechToTextConfigManager: + @classmethod + def convert(cls, config: dict) -> bool: + """ + Convert model config to model config + :param config: model config args + """ + speech_to_text = False + speech_to_text_dict = config.get('speech_to_text') + if speech_to_text_dict: + if 'enabled' in speech_to_text_dict and speech_to_text_dict['enabled']: + speech_to_text = True + + return speech_to_text -class SpeechToTextValidator: @classmethod def validate_and_set_defaults(cls, config: dict) -> tuple[dict, list[str]]: """ diff --git a/api/core/app/app_config/features/suggested_questions_after_answer/__init__.py b/api/core/app/app_config/features/suggested_questions_after_answer/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/app/validators/suggested_questions.py b/api/core/app/app_config/features/suggested_questions_after_answer/manager.py similarity index 57% rename from api/core/app/validators/suggested_questions.py rename to api/core/app/app_config/features/suggested_questions_after_answer/manager.py index 9161b31678..5aacd3b32d 100644 --- a/api/core/app/validators/suggested_questions.py +++ b/api/core/app/app_config/features/suggested_questions_after_answer/manager.py @@ -1,6 +1,19 @@ +class SuggestedQuestionsAfterAnswerConfigManager: + @classmethod + def convert(cls, config: dict) -> bool: + """ + Convert model config to model config + :param config: model config args + """ + suggested_questions_after_answer = False + suggested_questions_after_answer_dict = config.get('suggested_questions_after_answer') + if suggested_questions_after_answer_dict: + if 'enabled' in suggested_questions_after_answer_dict and suggested_questions_after_answer_dict['enabled']: + suggested_questions_after_answer = True + + return suggested_questions_after_answer -class SuggestedQuestionsValidator: @classmethod def validate_and_set_defaults(cls, config: dict) -> tuple[dict, list[str]]: """ @@ -16,7 +29,8 @@ class SuggestedQuestionsValidator: 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"]: + 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): diff --git a/api/core/app/app_config/features/text_to_speech/__init__.py b/api/core/app/app_config/features/text_to_speech/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/app/validators/text_to_speech.py b/api/core/app/app_config/features/text_to_speech/manager.py similarity index 56% rename from api/core/app/validators/text_to_speech.py rename to api/core/app/app_config/features/text_to_speech/manager.py index 182a912d52..1ff31034ad 100644 --- a/api/core/app/validators/text_to_speech.py +++ b/api/core/app/app_config/features/text_to_speech/manager.py @@ -1,6 +1,26 @@ +from core.app.app_config.entities import TextToSpeechEntity -class TextToSpeechValidator: +class TextToSpeechConfigManager: + @classmethod + def convert(cls, config: dict) -> bool: + """ + Convert model config to model config + + :param config: model config args + """ + text_to_speech = False + text_to_speech_dict = config.get('text_to_speech') + if text_to_speech_dict: + if 'enabled' in text_to_speech_dict and text_to_speech_dict['enabled']: + text_to_speech = TextToSpeechEntity( + enabled=text_to_speech_dict.get('enabled'), + voice=text_to_speech_dict.get('voice'), + language=text_to_speech_dict.get('language'), + ) + + return text_to_speech + @classmethod def validate_and_set_defaults(cls, config: dict) -> tuple[dict, list[str]]: """ diff --git a/api/core/app/app_config/workflow_ui_based_app/__init__.py b/api/core/app/app_config/workflow_ui_based_app/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/app/app_config/workflow_ui_based_app/variables/__init__.py b/api/core/app/app_config/workflow_ui_based_app/variables/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/app/app_config/workflow_ui_based_app/variables/manager.py b/api/core/app/app_config/workflow_ui_based_app/variables/manager.py new file mode 100644 index 0000000000..4b117d87f8 --- /dev/null +++ b/api/core/app/app_config/workflow_ui_based_app/variables/manager.py @@ -0,0 +1,22 @@ +from core.app.app_config.entities import VariableEntity +from models.workflow import Workflow + + +class WorkflowVariablesConfigManager: + @classmethod + def convert(cls, workflow: Workflow) -> list[VariableEntity]: + """ + Convert workflow start variables to variables + + :param workflow: workflow instance + """ + variables = [] + + # find start node + user_input_form = workflow.user_input_form() + + # variables + for variable in user_input_form: + variables.append(VariableEntity(**variable)) + + return variables diff --git a/api/core/app/app_manager.py b/api/core/app/app_manager.py index 86c8d2cfc7..98ebe2c87d 100644 --- a/api/core/app/app_manager.py +++ b/api/core/app/app_manager.py @@ -8,13 +8,18 @@ from typing import Any, Optional, Union, cast from flask import Flask, current_app from pydantic import ValidationError -from core.app.agent_chat.app_runner import AgentChatAppRunner -from core.app.app_orchestration_config_converter import AppOrchestrationConfigConverter +from core.app.app_config.easy_ui_based_app.model_config.converter import EasyUIBasedModelConfigEntityConverter +from core.app.app_config.entities import EasyUIBasedAppModelConfigFrom, EasyUIBasedAppConfig, VariableEntity +from core.app.apps.agent_chat.app_config_manager import AgentChatAppConfigManager +from core.app.apps.agent_chat.app_runner import AgentChatAppRunner from core.app.app_queue_manager import AppQueueManager, ConversationTaskStoppedException, PublishFrom -from core.app.chat.app_runner import ChatAppRunner +from core.app.apps.chat.app_config_manager import ChatAppConfigManager +from core.app.apps.chat.app_runner import ChatAppRunner +from core.app.apps.completion.app_config_manager import CompletionAppConfigManager +from core.app.apps.completion.app_runner import CompletionAppRunner from core.app.generate_task_pipeline import GenerateTaskPipeline -from core.entities.application_entities import ( - ApplicationGenerateEntity, +from core.app.entities.app_invoke_entities import ( + EasyUIBasedAppGenerateEntity, InvokeFrom, ) from core.file.file_obj import FileObj @@ -23,24 +28,19 @@ from core.model_runtime.model_providers.__base.large_language_model import Large from core.prompt.utils.prompt_template_parser import PromptTemplateParser from extensions.ext_database import db from models.account import Account -from models.model import App, Conversation, EndUser, Message, MessageFile +from models.model import App, Conversation, EndUser, Message, MessageFile, AppMode, AppModelConfig logger = logging.getLogger(__name__) -class AppManager: - """ - This class is responsible for managing application - """ +class EasyUIBasedAppManager: - def generate(self, tenant_id: str, - app_id: str, - app_model_config_id: str, - app_model_config_dict: dict, - app_model_config_override: bool, + def generate(self, app_model: App, + app_model_config: AppModelConfig, user: Union[Account, EndUser], invoke_from: InvokeFrom, inputs: dict[str, str], + app_model_config_dict: Optional[dict] = None, query: Optional[str] = None, files: Optional[list[FileObj]] = None, conversation: Optional[Conversation] = None, @@ -50,14 +50,12 @@ class AppManager: """ Generate App response. - :param tenant_id: workspace ID - :param app_id: app ID - :param app_model_config_id: app model config id - :param app_model_config_dict: app model config dict - :param app_model_config_override: app model config override + :param app_model: App + :param app_model_config: app model config :param user: account or end user :param invoke_from: invoke from source :param inputs: inputs + :param app_model_config_dict: app model config dict :param query: query :param files: file obj list :param conversation: conversation @@ -67,20 +65,21 @@ class AppManager: # init task id task_id = str(uuid.uuid4()) - # init application generate entity - application_generate_entity = ApplicationGenerateEntity( - task_id=task_id, - tenant_id=tenant_id, - app_id=app_id, - app_model_config_id=app_model_config_id, + # convert to app config + app_config = self.convert_to_app_config( + app_model=app_model, + app_model_config=app_model_config, app_model_config_dict=app_model_config_dict, - app_orchestration_config_entity=AppOrchestrationConfigConverter.convert_from_app_model_config_dict( - tenant_id=tenant_id, - app_model_config_dict=app_model_config_dict - ), - app_model_config_override=app_model_config_override, + conversation=conversation + ) + + # init application generate entity + application_generate_entity = EasyUIBasedAppGenerateEntity( + task_id=task_id, + app_config=app_config, + model_config=EasyUIBasedModelConfigEntityConverter.convert(app_config), conversation_id=conversation.id if conversation else None, - inputs=conversation.inputs if conversation else inputs, + inputs=conversation.inputs if conversation else self._get_cleaned_inputs(inputs, app_config), query=query.replace('\x00', '') if query else None, files=files if files else [], user_id=user.id, @@ -89,7 +88,7 @@ class AppManager: extras=extras ) - if not stream and application_generate_entity.app_orchestration_config_entity.agent: + if not stream and application_generate_entity.app_config.app_mode == AppMode.AGENT_CHAT: raise ValueError("Agent app is not supported in blocking mode.") # init generate records @@ -128,8 +127,85 @@ class AppManager: stream=stream ) + def convert_to_app_config(self, app_model: App, + app_model_config: AppModelConfig, + app_model_config_dict: Optional[dict] = None, + conversation: Optional[Conversation] = None) -> EasyUIBasedAppConfig: + if app_model_config_dict: + config_from = EasyUIBasedAppModelConfigFrom.ARGS + elif conversation: + config_from = EasyUIBasedAppModelConfigFrom.CONVERSATION_SPECIFIC_CONFIG + else: + config_from = EasyUIBasedAppModelConfigFrom.APP_LATEST_CONFIG + + app_mode = AppMode.value_of(app_model.mode) + if app_mode == AppMode.AGENT_CHAT or app_model.is_agent: + app_model.mode = AppMode.AGENT_CHAT.value + app_config = AgentChatAppConfigManager.config_convert( + app_model=app_model, + config_from=config_from, + app_model_config=app_model_config, + config_dict=app_model_config_dict + ) + elif app_mode == AppMode.CHAT: + app_config = ChatAppConfigManager.config_convert( + app_model=app_model, + config_from=config_from, + app_model_config=app_model_config, + config_dict=app_model_config_dict + ) + elif app_mode == AppMode.COMPLETION: + app_config = CompletionAppConfigManager.config_convert( + app_model=app_model, + config_from=config_from, + app_model_config=app_model_config, + config_dict=app_model_config_dict + ) + else: + raise ValueError("Invalid app mode") + + return app_config + + def _get_cleaned_inputs(self, user_inputs: dict, app_config: EasyUIBasedAppConfig): + if user_inputs is None: + user_inputs = {} + + filtered_inputs = {} + + # Filter input variables from form configuration, handle required fields, default values, and option values + variables = app_config.variables + for variable_config in variables: + variable = variable_config.variable + + if variable not in user_inputs or not user_inputs[variable]: + if variable_config.required: + raise ValueError(f"{variable} is required in input form") + else: + filtered_inputs[variable] = variable_config.default if variable_config.default is not None else "" + continue + + value = user_inputs[variable] + + if value: + if not isinstance(value, str): + raise ValueError(f"{variable} in input form must be a string") + + if variable_config.type == VariableEntity.Type.SELECT: + options = variable_config.options if variable_config.options is not None else [] + if value not in options: + raise ValueError(f"{variable} in input form must be one of the following: {options}") + else: + if variable_config.max_length is not None: + max_length = variable_config.max_length + if len(value) > max_length: + raise ValueError(f'{variable} in input form must be less than {max_length} characters') + + filtered_inputs[variable] = value.replace('\x00', '') if value else None + + return filtered_inputs + def _generate_worker(self, flask_app: Flask, - application_generate_entity: ApplicationGenerateEntity, + application_generate_entity: EasyUIBasedAppGenerateEntity, queue_manager: AppQueueManager, conversation_id: str, message_id: str) -> None: @@ -148,7 +224,7 @@ class AppManager: conversation = self._get_conversation(conversation_id) message = self._get_message(message_id) - if application_generate_entity.app_orchestration_config_entity.agent: + if application_generate_entity.app_config.app_mode == AppMode.AGENT_CHAT: # agent app runner = AgentChatAppRunner() runner.run( @@ -157,8 +233,8 @@ class AppManager: conversation=conversation, message=message ) - else: - # basic app + elif application_generate_entity.app_config.app_mode == AppMode.CHAT: + # chatbot app runner = ChatAppRunner() runner.run( application_generate_entity=application_generate_entity, @@ -166,6 +242,16 @@ class AppManager: conversation=conversation, message=message ) + elif application_generate_entity.app_config.app_mode == AppMode.COMPLETION: + # completion app + runner = CompletionAppRunner() + runner.run( + application_generate_entity=application_generate_entity, + queue_manager=queue_manager, + message=message + ) + else: + raise ValueError("Invalid app mode") except ConversationTaskStoppedException: pass except InvokeAuthorizationError: @@ -184,7 +270,7 @@ class AppManager: finally: db.session.remove() - def _handle_response(self, application_generate_entity: ApplicationGenerateEntity, + def _handle_response(self, application_generate_entity: EasyUIBasedAppGenerateEntity, queue_manager: AppQueueManager, conversation: Conversation, message: Message, @@ -217,24 +303,24 @@ class AppManager: finally: db.session.remove() - def _init_generate_records(self, application_generate_entity: ApplicationGenerateEntity) \ + def _init_generate_records(self, application_generate_entity: EasyUIBasedAppGenerateEntity) \ -> tuple[Conversation, Message]: """ Initialize generate records :param application_generate_entity: application generate entity :return: """ - app_orchestration_config_entity = application_generate_entity.app_orchestration_config_entity - - model_type_instance = app_orchestration_config_entity.model_config.provider_model_bundle.model_type_instance + model_type_instance = application_generate_entity.model_config.provider_model_bundle.model_type_instance model_type_instance = cast(LargeLanguageModel, model_type_instance) model_schema = model_type_instance.get_model_schema( - model=app_orchestration_config_entity.model_config.model, - credentials=app_orchestration_config_entity.model_config.credentials + model=application_generate_entity.model_config.model, + credentials=application_generate_entity.model_config.credentials ) + app_config = application_generate_entity.app_config + app_record = (db.session.query(App) - .filter(App.id == application_generate_entity.app_id).first()) + .filter(App.id == app_config.app_id).first()) app_mode = app_record.mode @@ -249,8 +335,8 @@ class AppManager: account_id = application_generate_entity.user_id override_model_configs = None - if application_generate_entity.app_model_config_override: - override_model_configs = application_generate_entity.app_model_config_dict + if app_config.app_model_config_from == EasyUIBasedAppModelConfigFrom.ARGS: + override_model_configs = app_config.app_model_config_dict introduction = '' if app_mode == 'chat': @@ -260,9 +346,9 @@ class AppManager: if not application_generate_entity.conversation_id: conversation = Conversation( app_id=app_record.id, - app_model_config_id=application_generate_entity.app_model_config_id, - model_provider=app_orchestration_config_entity.model_config.provider, - model_id=app_orchestration_config_entity.model_config.model, + app_model_config_id=app_config.app_model_config_id, + model_provider=application_generate_entity.model_config.provider, + model_id=application_generate_entity.model_config.model, override_model_configs=json.dumps(override_model_configs) if override_model_configs else None, mode=app_mode, name='New conversation', @@ -291,8 +377,8 @@ class AppManager: message = Message( app_id=app_record.id, - model_provider=app_orchestration_config_entity.model_config.provider, - model_id=app_orchestration_config_entity.model_config.model, + model_provider=application_generate_entity.model_config.provider, + model_id=application_generate_entity.model_config.model, override_model_configs=json.dumps(override_model_configs) if override_model_configs else None, conversation_id=conversation.id, inputs=application_generate_entity.inputs, @@ -311,7 +397,7 @@ class AppManager: from_source=from_source, from_end_user_id=end_user_id, from_account_id=account_id, - agent_based=app_orchestration_config_entity.agent is not None + agent_based=app_config.app_mode == AppMode.AGENT_CHAT, ) db.session.add(message) @@ -333,14 +419,14 @@ class AppManager: return conversation, message - def _get_conversation_introduction(self, application_generate_entity: ApplicationGenerateEntity) -> str: + def _get_conversation_introduction(self, application_generate_entity: EasyUIBasedAppGenerateEntity) -> str: """ Get conversation introduction :param application_generate_entity: application generate entity :return: conversation introduction """ - app_orchestration_config_entity = application_generate_entity.app_orchestration_config_entity - introduction = app_orchestration_config_entity.opening_statement + app_config = application_generate_entity.app_config + introduction = app_config.additional_features.opening_statement if introduction: try: diff --git a/api/core/app/app_orchestration_config_converter.py b/api/core/app/app_orchestration_config_converter.py deleted file mode 100644 index 1d429ee6d9..0000000000 --- a/api/core/app/app_orchestration_config_converter.py +++ /dev/null @@ -1,421 +0,0 @@ -from typing import cast - -from core.entities.application_entities import ( - AdvancedChatPromptTemplateEntity, - AdvancedCompletionPromptTemplateEntity, - AgentEntity, - AgentPromptEntity, - AgentToolEntity, - AppOrchestrationConfigEntity, - DatasetEntity, - DatasetRetrieveConfigEntity, - ExternalDataVariableEntity, - FileUploadEntity, - ModelConfigEntity, - PromptTemplateEntity, - SensitiveWordAvoidanceEntity, - TextToSpeechEntity, - VariableEntity, -) -from core.entities.model_entities import ModelStatus -from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError -from core.model_runtime.entities.message_entities import PromptMessageRole -from core.model_runtime.entities.model_entities import ModelType -from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel -from core.provider_manager import ProviderManager -from core.tools.prompt.template import REACT_PROMPT_TEMPLATES - - -class AppOrchestrationConfigConverter: - @classmethod - def convert_from_app_model_config_dict(cls, tenant_id: str, - app_model_config_dict: dict, - skip_check: bool = False) \ - -> AppOrchestrationConfigEntity: - """ - Convert app model config dict to entity. - :param tenant_id: tenant ID - :param app_model_config_dict: app model config dict - :param skip_check: skip check - :raises ProviderTokenNotInitError: provider token not init error - :return: app orchestration config entity - """ - properties = {} - - copy_app_model_config_dict = app_model_config_dict.copy() - - provider_manager = ProviderManager() - provider_model_bundle = provider_manager.get_provider_model_bundle( - tenant_id=tenant_id, - provider=copy_app_model_config_dict['model']['provider'], - model_type=ModelType.LLM - ) - - provider_name = provider_model_bundle.configuration.provider.provider - model_name = copy_app_model_config_dict['model']['name'] - - model_type_instance = provider_model_bundle.model_type_instance - model_type_instance = cast(LargeLanguageModel, model_type_instance) - - # check model credentials - model_credentials = provider_model_bundle.configuration.get_current_credentials( - model_type=ModelType.LLM, - model=copy_app_model_config_dict['model']['name'] - ) - - if model_credentials is None: - if not skip_check: - raise ProviderTokenNotInitError(f"Model {model_name} credentials is not initialized.") - else: - model_credentials = {} - - if not skip_check: - # check model - provider_model = provider_model_bundle.configuration.get_provider_model( - model=copy_app_model_config_dict['model']['name'], - model_type=ModelType.LLM - ) - - if provider_model is None: - model_name = copy_app_model_config_dict['model']['name'] - raise ValueError(f"Model {model_name} not exist.") - - if provider_model.status == ModelStatus.NO_CONFIGURE: - raise ProviderTokenNotInitError(f"Model {model_name} credentials is not initialized.") - elif provider_model.status == ModelStatus.NO_PERMISSION: - raise ModelCurrentlyNotSupportError(f"Dify Hosted OpenAI {model_name} currently not support.") - elif provider_model.status == ModelStatus.QUOTA_EXCEEDED: - raise QuotaExceededError(f"Model provider {provider_name} quota exceeded.") - - # model config - completion_params = copy_app_model_config_dict['model'].get('completion_params') - stop = [] - if 'stop' in completion_params: - stop = completion_params['stop'] - del completion_params['stop'] - - # get model mode - model_mode = copy_app_model_config_dict['model'].get('mode') - if not model_mode: - mode_enum = model_type_instance.get_model_mode( - model=copy_app_model_config_dict['model']['name'], - credentials=model_credentials - ) - - model_mode = mode_enum.value - - model_schema = model_type_instance.get_model_schema( - copy_app_model_config_dict['model']['name'], - model_credentials - ) - - if not skip_check and not model_schema: - raise ValueError(f"Model {model_name} not exist.") - - properties['model_config'] = ModelConfigEntity( - provider=copy_app_model_config_dict['model']['provider'], - model=copy_app_model_config_dict['model']['name'], - model_schema=model_schema, - mode=model_mode, - provider_model_bundle=provider_model_bundle, - credentials=model_credentials, - parameters=completion_params, - stop=stop, - ) - - # prompt template - prompt_type = PromptTemplateEntity.PromptType.value_of(copy_app_model_config_dict['prompt_type']) - if prompt_type == PromptTemplateEntity.PromptType.SIMPLE: - simple_prompt_template = copy_app_model_config_dict.get("pre_prompt", "") - properties['prompt_template'] = PromptTemplateEntity( - prompt_type=prompt_type, - simple_prompt_template=simple_prompt_template - ) - else: - advanced_chat_prompt_template = None - chat_prompt_config = copy_app_model_config_dict.get("chat_prompt_config", {}) - if chat_prompt_config: - chat_prompt_messages = [] - for message in chat_prompt_config.get("prompt", []): - chat_prompt_messages.append({ - "text": message["text"], - "role": PromptMessageRole.value_of(message["role"]) - }) - - advanced_chat_prompt_template = AdvancedChatPromptTemplateEntity( - messages=chat_prompt_messages - ) - - advanced_completion_prompt_template = None - completion_prompt_config = copy_app_model_config_dict.get("completion_prompt_config", {}) - if completion_prompt_config: - completion_prompt_template_params = { - 'prompt': completion_prompt_config['prompt']['text'], - } - - if 'conversation_histories_role' in completion_prompt_config: - completion_prompt_template_params['role_prefix'] = { - 'user': completion_prompt_config['conversation_histories_role']['user_prefix'], - 'assistant': completion_prompt_config['conversation_histories_role']['assistant_prefix'] - } - - advanced_completion_prompt_template = AdvancedCompletionPromptTemplateEntity( - **completion_prompt_template_params - ) - - properties['prompt_template'] = PromptTemplateEntity( - prompt_type=prompt_type, - advanced_chat_prompt_template=advanced_chat_prompt_template, - advanced_completion_prompt_template=advanced_completion_prompt_template - ) - - # external data variables - properties['external_data_variables'] = [] - - # old external_data_tools - external_data_tools = copy_app_model_config_dict.get('external_data_tools', []) - for external_data_tool in external_data_tools: - if 'enabled' not in external_data_tool or not external_data_tool['enabled']: - continue - - properties['external_data_variables'].append( - ExternalDataVariableEntity( - variable=external_data_tool['variable'], - type=external_data_tool['type'], - config=external_data_tool['config'] - ) - ) - - properties['variables'] = [] - - # variables and external_data_tools - for variable in copy_app_model_config_dict.get('user_input_form', []): - typ = list(variable.keys())[0] - if typ == 'external_data_tool': - val = variable[typ] - properties['external_data_variables'].append( - ExternalDataVariableEntity( - variable=val['variable'], - type=val['type'], - config=val['config'] - ) - ) - elif typ in [ - VariableEntity.Type.TEXT_INPUT.value, - VariableEntity.Type.PARAGRAPH.value, - VariableEntity.Type.NUMBER.value, - ]: - properties['variables'].append( - VariableEntity( - type=VariableEntity.Type.value_of(typ), - variable=variable[typ].get('variable'), - description=variable[typ].get('description'), - label=variable[typ].get('label'), - required=variable[typ].get('required', False), - max_length=variable[typ].get('max_length'), - default=variable[typ].get('default'), - ) - ) - elif typ == VariableEntity.Type.SELECT.value: - properties['variables'].append( - VariableEntity( - type=VariableEntity.Type.SELECT, - variable=variable[typ].get('variable'), - description=variable[typ].get('description'), - label=variable[typ].get('label'), - required=variable[typ].get('required', False), - options=variable[typ].get('options'), - default=variable[typ].get('default'), - ) - ) - - # show retrieve source - show_retrieve_source = False - retriever_resource_dict = copy_app_model_config_dict.get('retriever_resource') - if retriever_resource_dict: - if 'enabled' in retriever_resource_dict and retriever_resource_dict['enabled']: - show_retrieve_source = True - - properties['show_retrieve_source'] = show_retrieve_source - - dataset_ids = [] - if 'datasets' in copy_app_model_config_dict.get('dataset_configs', {}): - datasets = copy_app_model_config_dict.get('dataset_configs', {}).get('datasets', { - 'strategy': 'router', - 'datasets': [] - }) - - for dataset in datasets.get('datasets', []): - keys = list(dataset.keys()) - if len(keys) == 0 or keys[0] != 'dataset': - continue - dataset = dataset['dataset'] - - if 'enabled' not in dataset or not dataset['enabled']: - continue - - dataset_id = dataset.get('id', None) - if dataset_id: - dataset_ids.append(dataset_id) - - if 'agent_mode' in copy_app_model_config_dict and copy_app_model_config_dict['agent_mode'] \ - and 'enabled' in copy_app_model_config_dict['agent_mode'] \ - and copy_app_model_config_dict['agent_mode']['enabled']: - - agent_dict = copy_app_model_config_dict.get('agent_mode', {}) - agent_strategy = agent_dict.get('strategy', 'cot') - - if agent_strategy == 'function_call': - strategy = AgentEntity.Strategy.FUNCTION_CALLING - elif agent_strategy == 'cot' or agent_strategy == 'react': - strategy = AgentEntity.Strategy.CHAIN_OF_THOUGHT - else: - # old configs, try to detect default strategy - if copy_app_model_config_dict['model']['provider'] == 'openai': - strategy = AgentEntity.Strategy.FUNCTION_CALLING - else: - strategy = AgentEntity.Strategy.CHAIN_OF_THOUGHT - - agent_tools = [] - for tool in agent_dict.get('tools', []): - keys = tool.keys() - if len(keys) >= 4: - if "enabled" not in tool or not tool["enabled"]: - continue - - agent_tool_properties = { - 'provider_type': tool['provider_type'], - 'provider_id': tool['provider_id'], - 'tool_name': tool['tool_name'], - 'tool_parameters': tool['tool_parameters'] if 'tool_parameters' in tool else {} - } - - agent_tools.append(AgentToolEntity(**agent_tool_properties)) - elif len(keys) == 1: - # old standard - key = list(tool.keys())[0] - - if key != 'dataset': - continue - - tool_item = tool[key] - - if "enabled" not in tool_item or not tool_item["enabled"]: - continue - - dataset_id = tool_item['id'] - dataset_ids.append(dataset_id) - - if 'strategy' in copy_app_model_config_dict['agent_mode'] and \ - copy_app_model_config_dict['agent_mode']['strategy'] not in ['react_router', 'router']: - agent_prompt = agent_dict.get('prompt', None) or {} - # check model mode - model_mode = copy_app_model_config_dict.get('model', {}).get('mode', 'completion') - if model_mode == 'completion': - agent_prompt_entity = AgentPromptEntity( - first_prompt=agent_prompt.get('first_prompt', - REACT_PROMPT_TEMPLATES['english']['completion']['prompt']), - next_iteration=agent_prompt.get('next_iteration', - REACT_PROMPT_TEMPLATES['english']['completion'][ - 'agent_scratchpad']), - ) - else: - agent_prompt_entity = AgentPromptEntity( - first_prompt=agent_prompt.get('first_prompt', - REACT_PROMPT_TEMPLATES['english']['chat']['prompt']), - next_iteration=agent_prompt.get('next_iteration', - REACT_PROMPT_TEMPLATES['english']['chat']['agent_scratchpad']), - ) - - properties['agent'] = AgentEntity( - provider=properties['model_config'].provider, - model=properties['model_config'].model, - strategy=strategy, - prompt=agent_prompt_entity, - tools=agent_tools, - max_iteration=agent_dict.get('max_iteration', 5) - ) - - if len(dataset_ids) > 0: - # dataset configs - dataset_configs = copy_app_model_config_dict.get('dataset_configs', {'retrieval_model': 'single'}) - query_variable = copy_app_model_config_dict.get('dataset_query_variable') - - if dataset_configs['retrieval_model'] == 'single': - properties['dataset'] = DatasetEntity( - dataset_ids=dataset_ids, - retrieve_config=DatasetRetrieveConfigEntity( - query_variable=query_variable, - retrieve_strategy=DatasetRetrieveConfigEntity.RetrieveStrategy.value_of( - dataset_configs['retrieval_model'] - ) - ) - ) - else: - properties['dataset'] = DatasetEntity( - dataset_ids=dataset_ids, - retrieve_config=DatasetRetrieveConfigEntity( - query_variable=query_variable, - retrieve_strategy=DatasetRetrieveConfigEntity.RetrieveStrategy.value_of( - dataset_configs['retrieval_model'] - ), - top_k=dataset_configs.get('top_k'), - score_threshold=dataset_configs.get('score_threshold'), - reranking_model=dataset_configs.get('reranking_model') - ) - ) - - # file upload - file_upload_dict = copy_app_model_config_dict.get('file_upload') - if file_upload_dict: - if 'image' in file_upload_dict and file_upload_dict['image']: - if 'enabled' in file_upload_dict['image'] and file_upload_dict['image']['enabled']: - properties['file_upload'] = FileUploadEntity( - image_config={ - 'number_limits': file_upload_dict['image']['number_limits'], - 'detail': file_upload_dict['image']['detail'], - 'transfer_methods': file_upload_dict['image']['transfer_methods'] - } - ) - - # opening statement - properties['opening_statement'] = copy_app_model_config_dict.get('opening_statement') - - # suggested questions after answer - suggested_questions_after_answer_dict = copy_app_model_config_dict.get('suggested_questions_after_answer') - if suggested_questions_after_answer_dict: - if 'enabled' in suggested_questions_after_answer_dict and suggested_questions_after_answer_dict['enabled']: - properties['suggested_questions_after_answer'] = True - - # more like this - more_like_this_dict = copy_app_model_config_dict.get('more_like_this') - if more_like_this_dict: - if 'enabled' in more_like_this_dict and more_like_this_dict['enabled']: - properties['more_like_this'] = True - - # speech to text - speech_to_text_dict = copy_app_model_config_dict.get('speech_to_text') - if speech_to_text_dict: - if 'enabled' in speech_to_text_dict and speech_to_text_dict['enabled']: - properties['speech_to_text'] = True - - # text to speech - text_to_speech_dict = copy_app_model_config_dict.get('text_to_speech') - if text_to_speech_dict: - if 'enabled' in text_to_speech_dict and text_to_speech_dict['enabled']: - properties['text_to_speech'] = TextToSpeechEntity( - enabled=text_to_speech_dict.get('enabled'), - voice=text_to_speech_dict.get('voice'), - language=text_to_speech_dict.get('language'), - ) - - # sensitive word avoidance - sensitive_word_avoidance_dict = copy_app_model_config_dict.get('sensitive_word_avoidance') - if sensitive_word_avoidance_dict: - if 'enabled' in sensitive_word_avoidance_dict and sensitive_word_avoidance_dict['enabled']: - properties['sensitive_word_avoidance'] = SensitiveWordAvoidanceEntity( - type=sensitive_word_avoidance_dict.get('type'), - config=sensitive_word_avoidance_dict.get('config'), - ) - - return AppOrchestrationConfigEntity(**properties) diff --git a/api/core/app/app_queue_manager.py b/api/core/app/app_queue_manager.py index c09cae3245..4bd491269c 100644 --- a/api/core/app/app_queue_manager.py +++ b/api/core/app/app_queue_manager.py @@ -6,8 +6,8 @@ from typing import Any from sqlalchemy.orm import DeclarativeMeta -from core.entities.application_entities import InvokeFrom -from core.entities.queue_entities import ( +from core.app.entities.app_invoke_entities import InvokeFrom +from core.app.entities.queue_entities import ( AnnotationReplyEvent, AppQueueEvent, QueueAgentMessageEvent, diff --git a/api/core/app/apps/__init__.py b/api/core/app/apps/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/app/apps/advanced_chat/__init__.py b/api/core/app/apps/advanced_chat/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/app/apps/advanced_chat/app_config_manager.py b/api/core/app/apps/advanced_chat/app_config_manager.py new file mode 100644 index 0000000000..ab7857c4ad --- /dev/null +++ b/api/core/app/apps/advanced_chat/app_config_manager.py @@ -0,0 +1,94 @@ +from core.app.app_config.base_app_config_manager import BaseAppConfigManager +from core.app.app_config.common.sensitive_word_avoidance.manager import SensitiveWordAvoidanceConfigManager +from core.app.app_config.entities import WorkflowUIBasedAppConfig +from core.app.app_config.features.file_upload.manager import FileUploadConfigManager +from core.app.app_config.features.opening_statement.manager import OpeningStatementConfigManager +from core.app.app_config.features.retrieval_resource.manager import RetrievalResourceConfigManager +from core.app.app_config.features.speech_to_text.manager import SpeechToTextConfigManager +from core.app.app_config.features.suggested_questions_after_answer.manager import \ + SuggestedQuestionsAfterAnswerConfigManager +from core.app.app_config.features.text_to_speech.manager import TextToSpeechConfigManager +from core.app.app_config.workflow_ui_based_app.variables.manager import WorkflowVariablesConfigManager +from models.model import AppMode, App +from models.workflow import Workflow + + +class AdvancedChatAppConfig(WorkflowUIBasedAppConfig): + """ + Advanced Chatbot App Config Entity. + """ + pass + + +class AdvancedChatAppConfigManager(BaseAppConfigManager): + @classmethod + def config_convert(cls, app_model: App, workflow: Workflow) -> AdvancedChatAppConfig: + features_dict = workflow.features_dict + + app_config = AdvancedChatAppConfig( + tenant_id=app_model.tenant_id, + app_id=app_model.id, + app_mode=AppMode.value_of(app_model.mode), + workflow_id=workflow.id, + sensitive_word_avoidance=SensitiveWordAvoidanceConfigManager.convert( + config=features_dict + ), + variables=WorkflowVariablesConfigManager.convert( + workflow=workflow + ), + additional_features=cls.convert_features(features_dict) + ) + + return app_config + + @classmethod + def config_validate(cls, tenant_id: str, config: dict, only_structure_validate: bool = False) -> dict: + """ + Validate for advanced chat app model config + + :param tenant_id: tenant id + :param config: app model config args + :param only_structure_validate: if True, only structure validation will be performed + """ + related_config_keys = [] + + # file upload validation + config, current_related_config_keys = FileUploadConfigManager.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # opening_statement + config, current_related_config_keys = OpeningStatementConfigManager.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # suggested_questions_after_answer + config, current_related_config_keys = SuggestedQuestionsAfterAnswerConfigManager.validate_and_set_defaults( + config) + related_config_keys.extend(current_related_config_keys) + + # speech_to_text + config, current_related_config_keys = SpeechToTextConfigManager.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # text_to_speech + config, current_related_config_keys = TextToSpeechConfigManager.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # return retriever resource + config, current_related_config_keys = RetrievalResourceConfigManager.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # moderation validation + config, current_related_config_keys = SensitiveWordAvoidanceConfigManager.validate_and_set_defaults( + tenant_id=tenant_id, + config=config, + only_structure_validate=only_structure_validate + ) + 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/app/apps/agent_chat/__init__.py b/api/core/app/apps/agent_chat/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/app/agent_chat/config_validator.py b/api/core/app/apps/agent_chat/app_config_manager.py similarity index 51% rename from api/core/app/agent_chat/config_validator.py rename to api/core/app/apps/agent_chat/app_config_manager.py index 82bc40bd9b..96dac4bd01 100644 --- a/api/core/app/agent_chat/config_validator.py +++ b/api/core/app/apps/agent_chat/app_config_manager.py @@ -1,24 +1,82 @@ import uuid +from typing import Optional -from core.app.validators.dataset_retrieval import DatasetValidator -from core.app.validators.external_data_fetch import ExternalDataFetchValidator -from core.app.validators.file_upload import FileUploadValidator -from core.app.validators.model_validator import ModelValidator -from core.app.validators.moderation import ModerationValidator -from core.app.validators.opening_statement import OpeningStatementValidator -from core.app.validators.prompt import PromptValidator -from core.app.validators.retriever_resource import RetrieverResourceValidator -from core.app.validators.speech_to_text import SpeechToTextValidator -from core.app.validators.suggested_questions import SuggestedQuestionsValidator -from core.app.validators.text_to_speech import TextToSpeechValidator -from core.app.validators.user_input_form import UserInputFormValidator +from core.agent.entities import AgentEntity +from core.app.app_config.base_app_config_manager import BaseAppConfigManager +from core.app.app_config.easy_ui_based_app.agent.manager import AgentConfigManager +from core.app.app_config.easy_ui_based_app.dataset.manager import DatasetConfigManager +from core.app.app_config.easy_ui_based_app.model_config.manager import ModelConfigManager +from core.app.app_config.easy_ui_based_app.prompt_template.manager import PromptTemplateConfigManager +from core.app.app_config.easy_ui_based_app.variables.manager import BasicVariablesConfigManager +from core.app.app_config.common.sensitive_word_avoidance.manager import SensitiveWordAvoidanceConfigManager +from core.app.app_config.entities import EasyUIBasedAppConfig, EasyUIBasedAppModelConfigFrom, DatasetEntity +from core.app.app_config.features.file_upload.manager import FileUploadConfigManager +from core.app.app_config.features.opening_statement.manager import OpeningStatementConfigManager +from core.app.app_config.features.retrieval_resource.manager import RetrievalResourceConfigManager +from core.app.app_config.features.speech_to_text.manager import SpeechToTextConfigManager +from core.app.app_config.features.suggested_questions_after_answer.manager import \ + SuggestedQuestionsAfterAnswerConfigManager +from core.app.app_config.features.text_to_speech.manager import TextToSpeechConfigManager from core.entities.agent_entities import PlanningStrategy -from models.model import AppMode +from models.model import AppMode, App, AppModelConfig OLD_TOOLS = ["dataset", "google_search", "web_reader", "wikipedia", "current_datetime"] -class AgentChatAppConfigValidator: +class AgentChatAppConfig(EasyUIBasedAppConfig): + """ + Agent Chatbot App Config Entity. + """ + agent: Optional[AgentEntity] = None + + +class AgentChatAppConfigManager(BaseAppConfigManager): + @classmethod + def config_convert(cls, app_model: App, + config_from: EasyUIBasedAppModelConfigFrom, + app_model_config: AppModelConfig, + config_dict: Optional[dict] = None) -> AgentChatAppConfig: + """ + Convert app model config to agent chat app config + :param app_model: app model + :param config_from: app model config from + :param app_model_config: app model config + :param config_dict: app model config dict + :return: + """ + config_dict = cls.convert_to_config_dict(config_from, app_model_config, config_dict) + + app_config = AgentChatAppConfig( + tenant_id=app_model.tenant_id, + app_id=app_model.id, + app_mode=AppMode.value_of(app_model.mode), + app_model_config_from=config_from, + app_model_config_id=app_model_config.id, + app_model_config_dict=config_dict, + model=ModelConfigManager.convert( + config=config_dict + ), + prompt_template=PromptTemplateConfigManager.convert( + config=config_dict + ), + sensitive_word_avoidance=SensitiveWordAvoidanceConfigManager.convert( + config=config_dict + ), + dataset=DatasetConfigManager.convert( + config=config_dict + ), + agent=AgentConfigManager.convert( + config=config_dict + ), + additional_features=cls.convert_features(config_dict) + ) + + app_config.variables, app_config.external_data_variables = BasicVariablesConfigManager.convert( + config=config_dict + ) + + return app_config + @classmethod def config_validate(cls, tenant_id: str, config: dict) -> dict: """ @@ -32,23 +90,19 @@ class AgentChatAppConfigValidator: related_config_keys = [] # model - config, current_related_config_keys = ModelValidator.validate_and_set_defaults(tenant_id, config) + config, current_related_config_keys = ModelConfigManager.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 = ExternalDataFetchValidator.validate_and_set_defaults(tenant_id, config) + config, current_related_config_keys = BasicVariablesConfigManager.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) + config, current_related_config_keys = FileUploadConfigManager.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) + config, current_related_config_keys = PromptTemplateConfigManager.validate_and_set_defaults(app_mode, config) related_config_keys.extend(current_related_config_keys) # agent_mode @@ -56,27 +110,29 @@ class AgentChatAppConfigValidator: related_config_keys.extend(current_related_config_keys) # opening_statement - config, current_related_config_keys = OpeningStatementValidator.validate_and_set_defaults(config) + config, current_related_config_keys = OpeningStatementConfigManager.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) + config, current_related_config_keys = SuggestedQuestionsAfterAnswerConfigManager.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) + config, current_related_config_keys = SpeechToTextConfigManager.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) + config, current_related_config_keys = TextToSpeechConfigManager.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) + config, current_related_config_keys = RetrievalResourceConfigManager.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) + config, current_related_config_keys = SensitiveWordAvoidanceConfigManager.validate_and_set_defaults(tenant_id, + config) related_config_keys.extend(current_related_config_keys) related_config_keys = list(set(related_config_keys)) @@ -143,7 +199,7 @@ class AgentChatAppConfigValidator: except ValueError: raise ValueError("id in dataset must be of UUID type") - if not DatasetValidator.is_dataset_exists(tenant_id, tool_item["id"]): + if not DatasetConfigManager.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 diff --git a/api/core/app/agent_chat/app_runner.py b/api/core/app/apps/agent_chat/app_runner.py similarity index 83% rename from api/core/app/agent_chat/app_runner.py rename to api/core/app/apps/agent_chat/app_runner.py index 38789348ad..2f1de8f108 100644 --- a/api/core/app/agent_chat/app_runner.py +++ b/api/core/app/apps/agent_chat/app_runner.py @@ -2,10 +2,12 @@ import logging from typing import cast from core.agent.cot_agent_runner import CotAgentRunner +from core.agent.entities import AgentEntity from core.agent.fc_agent_runner import FunctionCallAgentRunner from core.app.app_queue_manager import AppQueueManager, PublishFrom -from core.app.base_app_runner import AppRunner -from core.entities.application_entities import AgentEntity, ApplicationGenerateEntity, ModelConfigEntity +from core.app.apps.agent_chat.app_config_manager import AgentChatAppConfig +from core.app.apps.base_app_runner import AppRunner +from core.app.entities.app_invoke_entities import EasyUIBasedAppGenerateEntity, EasyUIBasedModelConfigEntity from core.memory.token_buffer_memory import TokenBufferMemory from core.model_manager import ModelInstance from core.model_runtime.entities.llm_entities import LLMUsage @@ -24,7 +26,7 @@ class AgentChatAppRunner(AppRunner): """ Agent Application Runner """ - def run(self, application_generate_entity: ApplicationGenerateEntity, + def run(self, application_generate_entity: EasyUIBasedAppGenerateEntity, queue_manager: AppQueueManager, conversation: Conversation, message: Message) -> None: @@ -36,12 +38,13 @@ class AgentChatAppRunner(AppRunner): :param message: message :return: """ - app_record = db.session.query(App).filter(App.id == application_generate_entity.app_id).first() + app_config = application_generate_entity.app_config + app_config = cast(AgentChatAppConfig, app_config) + + app_record = db.session.query(App).filter(App.id == app_config.app_id).first() if not app_record: raise ValueError("App not found") - app_orchestration_config = application_generate_entity.app_orchestration_config_entity - inputs = application_generate_entity.inputs query = application_generate_entity.query files = application_generate_entity.files @@ -53,8 +56,8 @@ class AgentChatAppRunner(AppRunner): # Not Include: memory, external data, dataset context self.get_pre_calculate_rest_tokens( app_record=app_record, - model_config=app_orchestration_config.model_config, - prompt_template_entity=app_orchestration_config.prompt_template, + model_config=application_generate_entity.model_config, + prompt_template_entity=app_config.prompt_template, inputs=inputs, files=files, query=query @@ -64,22 +67,22 @@ class AgentChatAppRunner(AppRunner): if application_generate_entity.conversation_id: # get memory of conversation (read-only) model_instance = ModelInstance( - provider_model_bundle=app_orchestration_config.model_config.provider_model_bundle, - model=app_orchestration_config.model_config.model + provider_model_bundle=application_generate_entity.model_config.provider_model_bundle, + model=application_generate_entity.model_config.model ) memory = TokenBufferMemory( conversation=conversation, model_instance=model_instance ) - + # organize all inputs and template to prompt messages # Include: prompt template, inputs, query(optional), files(optional) # memory(optional) prompt_messages, _ = self.organize_prompt_messages( app_record=app_record, - model_config=app_orchestration_config.model_config, - prompt_template_entity=app_orchestration_config.prompt_template, + model_config=application_generate_entity.model_config, + prompt_template_entity=app_config.prompt_template, inputs=inputs, files=files, query=query, @@ -91,15 +94,15 @@ class AgentChatAppRunner(AppRunner): # process sensitive_word_avoidance _, inputs, query = self.moderation_for_inputs( app_id=app_record.id, - tenant_id=application_generate_entity.tenant_id, - app_orchestration_config_entity=app_orchestration_config, + tenant_id=app_config.tenant_id, + app_generate_entity=application_generate_entity, inputs=inputs, query=query, ) except ModerationException as e: self.direct_output( queue_manager=queue_manager, - app_orchestration_config=app_orchestration_config, + app_generate_entity=application_generate_entity, prompt_messages=prompt_messages, text=str(e), stream=application_generate_entity.stream @@ -123,7 +126,7 @@ class AgentChatAppRunner(AppRunner): ) self.direct_output( queue_manager=queue_manager, - app_orchestration_config=app_orchestration_config, + app_generate_entity=application_generate_entity, prompt_messages=prompt_messages, text=annotation_reply.content, stream=application_generate_entity.stream @@ -131,7 +134,7 @@ class AgentChatAppRunner(AppRunner): return # fill in variable inputs from external data tools if exists - external_data_tools = app_orchestration_config.external_data_variables + external_data_tools = app_config.external_data_variables if external_data_tools: inputs = self.fill_in_inputs_from_external_data_tools( tenant_id=app_record.tenant_id, @@ -146,8 +149,8 @@ class AgentChatAppRunner(AppRunner): # memory(optional), external data, dataset context(optional) prompt_messages, _ = self.organize_prompt_messages( app_record=app_record, - model_config=app_orchestration_config.model_config, - prompt_template_entity=app_orchestration_config.prompt_template, + model_config=application_generate_entity.model_config, + prompt_template_entity=app_config.prompt_template, inputs=inputs, files=files, query=query, @@ -164,25 +167,25 @@ class AgentChatAppRunner(AppRunner): if hosting_moderation_result: return - agent_entity = app_orchestration_config.agent + agent_entity = app_config.agent # load tool variables tool_conversation_variables = self._load_tool_variables(conversation_id=conversation.id, user_id=application_generate_entity.user_id, - tenant_id=application_generate_entity.tenant_id) + tenant_id=app_config.tenant_id) # convert db variables to tool variables tool_variables = self._convert_db_variables_to_tool_variables(tool_conversation_variables) # init model instance model_instance = ModelInstance( - provider_model_bundle=app_orchestration_config.model_config.provider_model_bundle, - model=app_orchestration_config.model_config.model + provider_model_bundle=application_generate_entity.model_config.provider_model_bundle, + model=application_generate_entity.model_config.model ) prompt_message, _ = self.organize_prompt_messages( app_record=app_record, - model_config=app_orchestration_config.model_config, - prompt_template_entity=app_orchestration_config.prompt_template, + model_config=application_generate_entity.model_config, + prompt_template_entity=app_config.prompt_template, inputs=inputs, files=files, query=query, @@ -203,10 +206,10 @@ class AgentChatAppRunner(AppRunner): # start agent runner if agent_entity.strategy == AgentEntity.Strategy.CHAIN_OF_THOUGHT: assistant_cot_runner = CotAgentRunner( - tenant_id=application_generate_entity.tenant_id, + tenant_id=app_config.tenant_id, application_generate_entity=application_generate_entity, - app_orchestration_config=app_orchestration_config, - model_config=app_orchestration_config.model_config, + app_config=app_config, + model_config=application_generate_entity.model_config, config=agent_entity, queue_manager=queue_manager, message=message, @@ -225,10 +228,10 @@ class AgentChatAppRunner(AppRunner): ) elif agent_entity.strategy == AgentEntity.Strategy.FUNCTION_CALLING: assistant_fc_runner = FunctionCallAgentRunner( - tenant_id=application_generate_entity.tenant_id, + tenant_id=app_config.tenant_id, application_generate_entity=application_generate_entity, - app_orchestration_config=app_orchestration_config, - model_config=app_orchestration_config.model_config, + app_config=app_config, + model_config=application_generate_entity.model_config, config=agent_entity, queue_manager=queue_manager, message=message, @@ -289,7 +292,7 @@ class AgentChatAppRunner(AppRunner): 'pool': db_variables.variables }) - def _get_usage_of_all_agent_thoughts(self, model_config: ModelConfigEntity, + def _get_usage_of_all_agent_thoughts(self, model_config: EasyUIBasedModelConfigEntity, message: Message) -> LLMUsage: """ Get usage of all agent thoughts diff --git a/api/core/app/base_app_runner.py b/api/core/app/apps/base_app_runner.py similarity index 93% rename from api/core/app/base_app_runner.py rename to api/core/app/apps/base_app_runner.py index 2760d04180..93f819af08 100644 --- a/api/core/app/base_app_runner.py +++ b/api/core/app/apps/base_app_runner.py @@ -2,16 +2,13 @@ import time from collections.abc import Generator from typing import Optional, Union, cast +from core.app.app_config.entities import PromptTemplateEntity, ExternalDataVariableEntity from core.app.app_queue_manager import AppQueueManager, PublishFrom from core.app.features.annotation_reply.annotation_reply import AnnotationReplyFeature from core.app.features.hosting_moderation.hosting_moderation import HostingModerationFeature -from core.entities.application_entities import ( - ApplicationGenerateEntity, - AppOrchestrationConfigEntity, - ExternalDataVariableEntity, - InvokeFrom, - ModelConfigEntity, - PromptTemplateEntity, +from core.app.entities.app_invoke_entities import ( + EasyUIBasedAppGenerateEntity, + InvokeFrom, EasyUIBasedModelConfigEntity, ) from core.external_data_tool.external_data_fetch import ExternalDataFetch from core.file.file_obj import FileObj @@ -29,7 +26,7 @@ from models.model import App, AppMode, Message, MessageAnnotation class AppRunner: def get_pre_calculate_rest_tokens(self, app_record: App, - model_config: ModelConfigEntity, + model_config: EasyUIBasedModelConfigEntity, prompt_template_entity: PromptTemplateEntity, inputs: dict[str, str], files: list[FileObj], @@ -85,7 +82,7 @@ class AppRunner: return rest_tokens - def recalc_llm_max_tokens(self, model_config: ModelConfigEntity, + def recale_llm_max_tokens(self, model_config: EasyUIBasedModelConfigEntity, prompt_messages: list[PromptMessage]): # recalc max_tokens if sum(prompt_token + max_tokens) over model token limit model_type_instance = model_config.provider_model_bundle.model_type_instance @@ -121,7 +118,7 @@ class AppRunner: model_config.parameters[parameter_rule.name] = max_tokens def organize_prompt_messages(self, app_record: App, - model_config: ModelConfigEntity, + model_config: EasyUIBasedModelConfigEntity, prompt_template_entity: PromptTemplateEntity, inputs: dict[str, str], files: list[FileObj], @@ -170,7 +167,7 @@ class AppRunner: return prompt_messages, stop def direct_output(self, queue_manager: AppQueueManager, - app_orchestration_config: AppOrchestrationConfigEntity, + app_generate_entity: EasyUIBasedAppGenerateEntity, prompt_messages: list, text: str, stream: bool, @@ -178,7 +175,7 @@ class AppRunner: """ Direct output :param queue_manager: application queue manager - :param app_orchestration_config: app orchestration config + :param app_generate_entity: app generate entity :param prompt_messages: prompt messages :param text: text :param stream: stream @@ -189,7 +186,7 @@ class AppRunner: index = 0 for token in text: queue_manager.publish_chunk_message(LLMResultChunk( - model=app_orchestration_config.model_config.model, + model=app_generate_entity.model_config.model, prompt_messages=prompt_messages, delta=LLMResultChunkDelta( index=index, @@ -201,7 +198,7 @@ class AppRunner: queue_manager.publish_message_end( llm_result=LLMResult( - model=app_orchestration_config.model_config.model, + model=app_generate_entity.model_config.model, prompt_messages=prompt_messages, message=AssistantPromptMessage(content=text), usage=usage if usage else LLMUsage.empty_usage() @@ -294,14 +291,14 @@ class AppRunner: def moderation_for_inputs(self, app_id: str, tenant_id: str, - app_orchestration_config_entity: AppOrchestrationConfigEntity, + app_generate_entity: EasyUIBasedAppGenerateEntity, inputs: dict, query: str) -> tuple[bool, dict, str]: """ Process sensitive_word_avoidance. :param app_id: app id :param tenant_id: tenant id - :param app_orchestration_config_entity: app orchestration config entity + :param app_generate_entity: app generate entity :param inputs: inputs :param query: query :return: @@ -310,12 +307,12 @@ class AppRunner: return moderation_feature.check( app_id=app_id, tenant_id=tenant_id, - app_orchestration_config_entity=app_orchestration_config_entity, + app_config=app_generate_entity.app_config, inputs=inputs, query=query, ) - def check_hosting_moderation(self, application_generate_entity: ApplicationGenerateEntity, + def check_hosting_moderation(self, application_generate_entity: EasyUIBasedAppGenerateEntity, queue_manager: AppQueueManager, prompt_messages: list[PromptMessage]) -> bool: """ @@ -334,7 +331,7 @@ class AppRunner: if moderation_result: self.direct_output( queue_manager=queue_manager, - app_orchestration_config=application_generate_entity.app_orchestration_config_entity, + app_generate_entity=application_generate_entity, prompt_messages=prompt_messages, text="I apologize for any confusion, " \ "but I'm an AI assistant to be helpful, harmless, and honest.", diff --git a/api/core/app/apps/chat/__init__.py b/api/core/app/apps/chat/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/app/apps/chat/app_config_manager.py b/api/core/app/apps/chat/app_config_manager.py new file mode 100644 index 0000000000..62b2aaae5a --- /dev/null +++ b/api/core/app/apps/chat/app_config_manager.py @@ -0,0 +1,135 @@ +from typing import Optional + +from core.app.app_config.base_app_config_manager import BaseAppConfigManager +from core.app.app_config.easy_ui_based_app.dataset.manager import DatasetConfigManager +from core.app.app_config.easy_ui_based_app.model_config.manager import ModelConfigManager +from core.app.app_config.easy_ui_based_app.prompt_template.manager import PromptTemplateConfigManager +from core.app.app_config.easy_ui_based_app.variables.manager import BasicVariablesConfigManager +from core.app.app_config.common.sensitive_word_avoidance.manager import SensitiveWordAvoidanceConfigManager +from core.app.app_config.entities import EasyUIBasedAppConfig, EasyUIBasedAppModelConfigFrom +from core.app.app_config.features.file_upload.manager import FileUploadConfigManager +from core.app.app_config.features.opening_statement.manager import OpeningStatementConfigManager +from core.app.app_config.features.retrieval_resource.manager import RetrievalResourceConfigManager +from core.app.app_config.features.speech_to_text.manager import SpeechToTextConfigManager +from core.app.app_config.features.suggested_questions_after_answer.manager import \ + SuggestedQuestionsAfterAnswerConfigManager +from core.app.app_config.features.text_to_speech.manager import TextToSpeechConfigManager +from models.model import AppMode, App, AppModelConfig + + +class ChatAppConfig(EasyUIBasedAppConfig): + """ + Chatbot App Config Entity. + """ + pass + + +class ChatAppConfigManager(BaseAppConfigManager): + @classmethod + def config_convert(cls, app_model: App, + config_from: EasyUIBasedAppModelConfigFrom, + app_model_config: AppModelConfig, + config_dict: Optional[dict] = None) -> ChatAppConfig: + """ + Convert app model config to chat app config + :param app_model: app model + :param config_from: app model config from + :param app_model_config: app model config + :param config_dict: app model config dict + :return: + """ + config_dict = cls.convert_to_config_dict(config_from, app_model_config, config_dict) + + app_config = ChatAppConfig( + tenant_id=app_model.tenant_id, + app_id=app_model.id, + app_mode=AppMode.value_of(app_model.mode), + app_model_config_from=config_from, + app_model_config_id=app_model_config.id, + app_model_config_dict=config_dict, + model=ModelConfigManager.convert( + config=config_dict + ), + prompt_template=PromptTemplateConfigManager.convert( + config=config_dict + ), + sensitive_word_avoidance=SensitiveWordAvoidanceConfigManager.convert( + config=config_dict + ), + dataset=DatasetConfigManager.convert( + config=config_dict + ), + additional_features=cls.convert_features(config_dict) + ) + + app_config.variables, app_config.external_data_variables = BasicVariablesConfigManager.convert( + config=config_dict + ) + + return app_config + + @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 = ModelConfigManager.validate_and_set_defaults(tenant_id, config) + related_config_keys.extend(current_related_config_keys) + + # user_input_form + config, current_related_config_keys = BasicVariablesConfigManager.validate_and_set_defaults(tenant_id, config) + related_config_keys.extend(current_related_config_keys) + + # file upload validation + config, current_related_config_keys = FileUploadConfigManager.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # prompt + config, current_related_config_keys = PromptTemplateConfigManager.validate_and_set_defaults(app_mode, config) + related_config_keys.extend(current_related_config_keys) + + # dataset_query_variable + config, current_related_config_keys = DatasetConfigManager.validate_and_set_defaults(tenant_id, app_mode, + config) + related_config_keys.extend(current_related_config_keys) + + # opening_statement + config, current_related_config_keys = OpeningStatementConfigManager.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # suggested_questions_after_answer + config, current_related_config_keys = SuggestedQuestionsAfterAnswerConfigManager.validate_and_set_defaults( + config) + related_config_keys.extend(current_related_config_keys) + + # speech_to_text + config, current_related_config_keys = SpeechToTextConfigManager.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # text_to_speech + config, current_related_config_keys = TextToSpeechConfigManager.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # return retriever resource + config, current_related_config_keys = RetrievalResourceConfigManager.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # moderation validation + config, current_related_config_keys = SensitiveWordAvoidanceConfigManager.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/app/chat/app_runner.py b/api/core/app/apps/chat/app_runner.py similarity index 76% rename from api/core/app/chat/app_runner.py rename to api/core/app/apps/chat/app_runner.py index 4c8018572e..403a2d4476 100644 --- a/api/core/app/chat/app_runner.py +++ b/api/core/app/apps/chat/app_runner.py @@ -1,10 +1,12 @@ import logging +from typing import cast from core.app.app_queue_manager import AppQueueManager, PublishFrom -from core.app.base_app_runner import AppRunner +from core.app.apps.chat.app_config_manager import ChatAppConfig +from core.app.apps.base_app_runner import AppRunner from core.callback_handler.index_tool_callback_handler import DatasetIndexToolCallbackHandler -from core.entities.application_entities import ( - ApplicationGenerateEntity, +from core.app.entities.app_invoke_entities import ( + EasyUIBasedAppGenerateEntity, ) from core.memory.token_buffer_memory import TokenBufferMemory from core.model_manager import ModelInstance @@ -21,7 +23,7 @@ class ChatAppRunner(AppRunner): Chat Application Runner """ - def run(self, application_generate_entity: ApplicationGenerateEntity, + def run(self, application_generate_entity: EasyUIBasedAppGenerateEntity, queue_manager: AppQueueManager, conversation: Conversation, message: Message) -> None: @@ -33,12 +35,13 @@ class ChatAppRunner(AppRunner): :param message: message :return: """ - app_record = db.session.query(App).filter(App.id == application_generate_entity.app_id).first() + app_config = application_generate_entity.app_config + app_config = cast(ChatAppConfig, app_config) + + app_record = db.session.query(App).filter(App.id == app_config.app_id).first() if not app_record: raise ValueError("App not found") - app_orchestration_config = application_generate_entity.app_orchestration_config_entity - inputs = application_generate_entity.inputs query = application_generate_entity.query files = application_generate_entity.files @@ -50,8 +53,8 @@ class ChatAppRunner(AppRunner): # Not Include: memory, external data, dataset context self.get_pre_calculate_rest_tokens( app_record=app_record, - model_config=app_orchestration_config.model_config, - prompt_template_entity=app_orchestration_config.prompt_template, + model_config=application_generate_entity.model_config, + prompt_template_entity=app_config.prompt_template, inputs=inputs, files=files, query=query @@ -61,8 +64,8 @@ class ChatAppRunner(AppRunner): if application_generate_entity.conversation_id: # get memory of conversation (read-only) model_instance = ModelInstance( - provider_model_bundle=app_orchestration_config.model_config.provider_model_bundle, - model=app_orchestration_config.model_config.model + provider_model_bundle=application_generate_entity.model_config.provider_model_bundle, + model=application_generate_entity.model_config.model ) memory = TokenBufferMemory( @@ -75,8 +78,8 @@ class ChatAppRunner(AppRunner): # memory(optional) prompt_messages, stop = self.organize_prompt_messages( app_record=app_record, - model_config=app_orchestration_config.model_config, - prompt_template_entity=app_orchestration_config.prompt_template, + model_config=application_generate_entity.model_config, + prompt_template_entity=app_config.prompt_template, inputs=inputs, files=files, query=query, @@ -88,15 +91,15 @@ class ChatAppRunner(AppRunner): # process sensitive_word_avoidance _, inputs, query = self.moderation_for_inputs( app_id=app_record.id, - tenant_id=application_generate_entity.tenant_id, - app_orchestration_config_entity=app_orchestration_config, + tenant_id=app_config.tenant_id, + app_generate_entity=application_generate_entity, inputs=inputs, query=query, ) except ModerationException as e: self.direct_output( queue_manager=queue_manager, - app_orchestration_config=app_orchestration_config, + app_generate_entity=application_generate_entity, prompt_messages=prompt_messages, text=str(e), stream=application_generate_entity.stream @@ -120,7 +123,7 @@ class ChatAppRunner(AppRunner): ) self.direct_output( queue_manager=queue_manager, - app_orchestration_config=app_orchestration_config, + app_generate_entity=application_generate_entity, prompt_messages=prompt_messages, text=annotation_reply.content, stream=application_generate_entity.stream @@ -128,7 +131,7 @@ class ChatAppRunner(AppRunner): return # fill in variable inputs from external data tools if exists - external_data_tools = app_orchestration_config.external_data_variables + external_data_tools = app_config.external_data_variables if external_data_tools: inputs = self.fill_in_inputs_from_external_data_tools( tenant_id=app_record.tenant_id, @@ -140,7 +143,7 @@ class ChatAppRunner(AppRunner): # get context from datasets context = None - if app_orchestration_config.dataset and app_orchestration_config.dataset.dataset_ids: + if app_config.dataset and app_config.dataset.dataset_ids: hit_callback = DatasetIndexToolCallbackHandler( queue_manager, app_record.id, @@ -152,11 +155,11 @@ class ChatAppRunner(AppRunner): dataset_retrieval = DatasetRetrieval() context = dataset_retrieval.retrieve( tenant_id=app_record.tenant_id, - model_config=app_orchestration_config.model_config, - config=app_orchestration_config.dataset, + model_config=application_generate_entity.model_config, + config=app_config.dataset, query=query, invoke_from=application_generate_entity.invoke_from, - show_retrieve_source=app_orchestration_config.show_retrieve_source, + show_retrieve_source=app_config.additional_features.show_retrieve_source, hit_callback=hit_callback, memory=memory ) @@ -166,8 +169,8 @@ class ChatAppRunner(AppRunner): # memory(optional), external data, dataset context(optional) prompt_messages, stop = self.organize_prompt_messages( app_record=app_record, - model_config=app_orchestration_config.model_config, - prompt_template_entity=app_orchestration_config.prompt_template, + model_config=application_generate_entity.model_config, + prompt_template_entity=app_config.prompt_template, inputs=inputs, files=files, query=query, @@ -186,22 +189,22 @@ class ChatAppRunner(AppRunner): return # Re-calculate the max tokens if sum(prompt_token + max_tokens) over model token limit - self.recalc_llm_max_tokens( - model_config=app_orchestration_config.model_config, + self.recale_llm_max_tokens( + model_config=application_generate_entity.model_config, prompt_messages=prompt_messages ) # Invoke model model_instance = ModelInstance( - provider_model_bundle=app_orchestration_config.model_config.provider_model_bundle, - model=app_orchestration_config.model_config.model + provider_model_bundle=application_generate_entity.model_config.provider_model_bundle, + model=application_generate_entity.model_config.model ) db.session.close() invoke_result = model_instance.invoke_llm( prompt_messages=prompt_messages, - model_parameters=app_orchestration_config.model_config.parameters, + model_parameters=application_generate_entity.model_config.parameters, stop=stop, stream=application_generate_entity.stream, user=application_generate_entity.user_id, diff --git a/api/core/app/apps/completion/__init__.py b/api/core/app/apps/completion/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/app/apps/completion/app_config_manager.py b/api/core/app/apps/completion/app_config_manager.py new file mode 100644 index 0000000000..b920f369b5 --- /dev/null +++ b/api/core/app/apps/completion/app_config_manager.py @@ -0,0 +1,118 @@ +from typing import Optional + +from core.app.app_config.base_app_config_manager import BaseAppConfigManager +from core.app.app_config.easy_ui_based_app.dataset.manager import DatasetConfigManager +from core.app.app_config.easy_ui_based_app.model_config.manager import ModelConfigManager +from core.app.app_config.easy_ui_based_app.prompt_template.manager import PromptTemplateConfigManager +from core.app.app_config.easy_ui_based_app.variables.manager import BasicVariablesConfigManager +from core.app.app_config.common.sensitive_word_avoidance.manager import SensitiveWordAvoidanceConfigManager +from core.app.app_config.entities import EasyUIBasedAppConfig, EasyUIBasedAppModelConfigFrom +from core.app.app_config.features.file_upload.manager import FileUploadConfigManager +from core.app.app_config.features.more_like_this.manager import MoreLikeThisConfigManager +from core.app.app_config.features.text_to_speech.manager import TextToSpeechConfigManager +from models.model import AppMode, App, AppModelConfig + + +class CompletionAppConfig(EasyUIBasedAppConfig): + """ + Completion App Config Entity. + """ + pass + + +class CompletionAppConfigManager(BaseAppConfigManager): + @classmethod + def config_convert(cls, app_model: App, + config_from: EasyUIBasedAppModelConfigFrom, + app_model_config: AppModelConfig, + config_dict: Optional[dict] = None) -> CompletionAppConfig: + """ + Convert app model config to completion app config + :param app_model: app model + :param config_from: app model config from + :param app_model_config: app model config + :param config_dict: app model config dict + :return: + """ + config_dict = cls.convert_to_config_dict(config_from, app_model_config, config_dict) + + app_config = CompletionAppConfig( + tenant_id=app_model.tenant_id, + app_id=app_model.id, + app_mode=AppMode.value_of(app_model.mode), + app_model_config_from=config_from, + app_model_config_id=app_model_config.id, + app_model_config_dict=config_dict, + model=ModelConfigManager.convert( + config=config_dict + ), + prompt_template=PromptTemplateConfigManager.convert( + config=config_dict + ), + sensitive_word_avoidance=SensitiveWordAvoidanceConfigManager.convert( + config=config_dict + ), + dataset=DatasetConfigManager.convert( + config=config_dict + ), + additional_features=cls.convert_features(config_dict) + ) + + app_config.variables, app_config.external_data_variables = BasicVariablesConfigManager.convert( + config=config_dict + ) + + return app_config + + @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 = ModelConfigManager.validate_and_set_defaults(tenant_id, config) + related_config_keys.extend(current_related_config_keys) + + # user_input_form + config, current_related_config_keys = BasicVariablesConfigManager.validate_and_set_defaults(tenant_id, config) + related_config_keys.extend(current_related_config_keys) + + # file upload validation + config, current_related_config_keys = FileUploadConfigManager.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # prompt + config, current_related_config_keys = PromptTemplateConfigManager.validate_and_set_defaults(app_mode, config) + related_config_keys.extend(current_related_config_keys) + + # dataset_query_variable + config, current_related_config_keys = DatasetConfigManager.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 = TextToSpeechConfigManager.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # more_like_this + config, current_related_config_keys = MoreLikeThisConfigManager.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # moderation validation + config, current_related_config_keys = SensitiveWordAvoidanceConfigManager.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/app/completion/app_runner.py b/api/core/app/apps/completion/app_runner.py similarity index 74% rename from api/core/app/completion/app_runner.py rename to api/core/app/apps/completion/app_runner.py index ab2f40ad9a..8f0f191d45 100644 --- a/api/core/app/completion/app_runner.py +++ b/api/core/app/apps/completion/app_runner.py @@ -1,10 +1,12 @@ import logging +from typing import cast from core.app.app_queue_manager import AppQueueManager -from core.app.base_app_runner import AppRunner +from core.app.apps.completion.app_config_manager import CompletionAppConfig +from core.app.apps.base_app_runner import AppRunner from core.callback_handler.index_tool_callback_handler import DatasetIndexToolCallbackHandler -from core.entities.application_entities import ( - ApplicationGenerateEntity, +from core.app.entities.app_invoke_entities import ( + EasyUIBasedAppGenerateEntity, ) from core.model_manager import ModelInstance from core.moderation.base import ModerationException @@ -20,7 +22,7 @@ class CompletionAppRunner(AppRunner): Completion Application Runner """ - def run(self, application_generate_entity: ApplicationGenerateEntity, + def run(self, application_generate_entity: EasyUIBasedAppGenerateEntity, queue_manager: AppQueueManager, message: Message) -> None: """ @@ -30,12 +32,13 @@ class CompletionAppRunner(AppRunner): :param message: message :return: """ - app_record = db.session.query(App).filter(App.id == application_generate_entity.app_id).first() + app_config = application_generate_entity.app_config + app_config = cast(CompletionAppConfig, app_config) + + app_record = db.session.query(App).filter(App.id == app_config.app_id).first() if not app_record: raise ValueError("App not found") - app_orchestration_config = application_generate_entity.app_orchestration_config_entity - inputs = application_generate_entity.inputs query = application_generate_entity.query files = application_generate_entity.files @@ -47,8 +50,8 @@ class CompletionAppRunner(AppRunner): # Not Include: memory, external data, dataset context self.get_pre_calculate_rest_tokens( app_record=app_record, - model_config=app_orchestration_config.model_config, - prompt_template_entity=app_orchestration_config.prompt_template, + model_config=application_generate_entity.model_config, + prompt_template_entity=app_config.prompt_template, inputs=inputs, files=files, query=query @@ -58,8 +61,8 @@ class CompletionAppRunner(AppRunner): # Include: prompt template, inputs, query(optional), files(optional) prompt_messages, stop = self.organize_prompt_messages( app_record=app_record, - model_config=app_orchestration_config.model_config, - prompt_template_entity=app_orchestration_config.prompt_template, + model_config=application_generate_entity.model_config, + prompt_template_entity=app_config.prompt_template, inputs=inputs, files=files, query=query @@ -70,15 +73,15 @@ class CompletionAppRunner(AppRunner): # process sensitive_word_avoidance _, inputs, query = self.moderation_for_inputs( app_id=app_record.id, - tenant_id=application_generate_entity.tenant_id, - app_orchestration_config_entity=app_orchestration_config, + tenant_id=app_config.tenant_id, + app_generate_entity=application_generate_entity, inputs=inputs, query=query, ) except ModerationException as e: self.direct_output( queue_manager=queue_manager, - app_orchestration_config=app_orchestration_config, + app_generate_entity=application_generate_entity, prompt_messages=prompt_messages, text=str(e), stream=application_generate_entity.stream @@ -86,7 +89,7 @@ class CompletionAppRunner(AppRunner): return # fill in variable inputs from external data tools if exists - external_data_tools = app_orchestration_config.external_data_variables + external_data_tools = app_config.external_data_variables if external_data_tools: inputs = self.fill_in_inputs_from_external_data_tools( tenant_id=app_record.tenant_id, @@ -98,7 +101,7 @@ class CompletionAppRunner(AppRunner): # get context from datasets context = None - if app_orchestration_config.dataset and app_orchestration_config.dataset.dataset_ids: + if app_config.dataset and app_config.dataset.dataset_ids: hit_callback = DatasetIndexToolCallbackHandler( queue_manager, app_record.id, @@ -107,18 +110,18 @@ class CompletionAppRunner(AppRunner): application_generate_entity.invoke_from ) - dataset_config = app_orchestration_config.dataset + dataset_config = app_config.dataset if dataset_config and dataset_config.retrieve_config.query_variable: query = inputs.get(dataset_config.retrieve_config.query_variable, "") dataset_retrieval = DatasetRetrieval() context = dataset_retrieval.retrieve( tenant_id=app_record.tenant_id, - model_config=app_orchestration_config.model_config, + model_config=application_generate_entity.model_config, config=dataset_config, query=query, invoke_from=application_generate_entity.invoke_from, - show_retrieve_source=app_orchestration_config.show_retrieve_source, + show_retrieve_source=app_config.additional_features.show_retrieve_source, hit_callback=hit_callback ) @@ -127,8 +130,8 @@ class CompletionAppRunner(AppRunner): # memory(optional), external data, dataset context(optional) prompt_messages, stop = self.organize_prompt_messages( app_record=app_record, - model_config=app_orchestration_config.model_config, - prompt_template_entity=app_orchestration_config.prompt_template, + model_config=application_generate_entity.model_config, + prompt_template_entity=app_config.prompt_template, inputs=inputs, files=files, query=query, @@ -147,19 +150,19 @@ class CompletionAppRunner(AppRunner): # Re-calculate the max tokens if sum(prompt_token + max_tokens) over model token limit self.recale_llm_max_tokens( - model_config=app_orchestration_config.model_config, + model_config=application_generate_entity.model_config, prompt_messages=prompt_messages ) # Invoke model model_instance = ModelInstance( - provider_model_bundle=app_orchestration_config.model_config.provider_model_bundle, - model=app_orchestration_config.model_config.model + provider_model_bundle=application_generate_entity.model_config.provider_model_bundle, + model=application_generate_entity.model_config.model ) invoke_result = model_instance.invoke_llm( prompt_messages=prompt_messages, - model_parameters=app_orchestration_config.model_config.parameters, + model_parameters=application_generate_entity.model_config.parameters, stop=stop, stream=application_generate_entity.stream, user=application_generate_entity.user_id, diff --git a/api/core/app/apps/workflow/__init__.py b/api/core/app/apps/workflow/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/app/apps/workflow/app_config_manager.py b/api/core/app/apps/workflow/app_config_manager.py new file mode 100644 index 0000000000..35da72b63e --- /dev/null +++ b/api/core/app/apps/workflow/app_config_manager.py @@ -0,0 +1,71 @@ +from core.app.app_config.base_app_config_manager import BaseAppConfigManager +from core.app.app_config.common.sensitive_word_avoidance.manager import SensitiveWordAvoidanceConfigManager +from core.app.app_config.entities import WorkflowUIBasedAppConfig +from core.app.app_config.features.file_upload.manager import FileUploadConfigManager +from core.app.app_config.features.text_to_speech.manager import TextToSpeechConfigManager +from core.app.app_config.workflow_ui_based_app.variables.manager import WorkflowVariablesConfigManager +from models.model import AppMode, App +from models.workflow import Workflow + + +class WorkflowAppConfig(WorkflowUIBasedAppConfig): + """ + Workflow App Config Entity. + """ + pass + + +class WorkflowAppConfigManager(BaseAppConfigManager): + @classmethod + def config_convert(cls, app_model: App, workflow: Workflow) -> WorkflowAppConfig: + features_dict = workflow.features_dict + + app_config = WorkflowAppConfig( + tenant_id=app_model.tenant_id, + app_id=app_model.id, + app_mode=AppMode.value_of(app_model.mode), + workflow_id=workflow.id, + sensitive_word_avoidance=SensitiveWordAvoidanceConfigManager.convert( + config=features_dict + ), + variables=WorkflowVariablesConfigManager.convert( + workflow=workflow + ), + additional_features=cls.convert_features(features_dict) + ) + + return app_config + + @classmethod + def config_validate(cls, tenant_id: str, config: dict, only_structure_validate: bool = False) -> dict: + """ + Validate for workflow app model config + + :param tenant_id: tenant id + :param config: app model config args + :param only_structure_validate: only validate the structure of the config + """ + related_config_keys = [] + + # file upload validation + config, current_related_config_keys = FileUploadConfigManager.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # text_to_speech + config, current_related_config_keys = TextToSpeechConfigManager.validate_and_set_defaults(config) + related_config_keys.extend(current_related_config_keys) + + # moderation validation + config, current_related_config_keys = SensitiveWordAvoidanceConfigManager.validate_and_set_defaults( + tenant_id=tenant_id, + config=config, + only_structure_validate=only_structure_validate + ) + 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/app/chat/config_validator.py b/api/core/app/chat/config_validator.py deleted file mode 100644 index adb8408e28..0000000000 --- a/api/core/app/chat/config_validator.py +++ /dev/null @@ -1,82 +0,0 @@ -from core.app.validators.dataset_retrieval import DatasetValidator -from core.app.validators.external_data_fetch import ExternalDataFetchValidator -from core.app.validators.file_upload import FileUploadValidator -from core.app.validators.model_validator import ModelValidator -from core.app.validators.moderation import ModerationValidator -from core.app.validators.opening_statement import OpeningStatementValidator -from core.app.validators.prompt import PromptValidator -from core.app.validators.retriever_resource import RetrieverResourceValidator -from core.app.validators.speech_to_text import SpeechToTextValidator -from core.app.validators.suggested_questions import SuggestedQuestionsValidator -from core.app.validators.text_to_speech import TextToSpeechValidator -from core.app.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 = ExternalDataFetchValidator.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/app/completion/config_validator.py b/api/core/app/completion/config_validator.py deleted file mode 100644 index 7cc35efd64..0000000000 --- a/api/core/app/completion/config_validator.py +++ /dev/null @@ -1,67 +0,0 @@ -from core.app.validators.dataset_retrieval import DatasetValidator -from core.app.validators.external_data_fetch import ExternalDataFetchValidator -from core.app.validators.file_upload import FileUploadValidator -from core.app.validators.model_validator import ModelValidator -from core.app.validators.moderation import ModerationValidator -from core.app.validators.more_like_this import MoreLikeThisValidator -from core.app.validators.prompt import PromptValidator -from core.app.validators.text_to_speech import TextToSpeechValidator -from core.app.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 = ExternalDataFetchValidator.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/app/entities/__init__.py b/api/core/app/entities/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/app/entities/app_invoke_entities.py b/api/core/app/entities/app_invoke_entities.py new file mode 100644 index 0000000000..fae9044fc3 --- /dev/null +++ b/api/core/app/entities/app_invoke_entities.py @@ -0,0 +1,111 @@ +from enum import Enum +from typing import Any, Optional + +from pydantic import BaseModel + +from core.app.app_config.entities import EasyUIBasedAppConfig, WorkflowUIBasedAppConfig +from core.entities.provider_configuration import ProviderModelBundle +from core.file.file_obj import FileObj +from core.model_runtime.entities.model_entities import AIModelEntity + + +class InvokeFrom(Enum): + """ + Invoke From. + """ + SERVICE_API = 'service-api' + WEB_APP = 'web-app' + EXPLORE = 'explore' + DEBUGGER = 'debugger' + + @classmethod + def value_of(cls, value: str) -> 'InvokeFrom': + """ + Get value of given mode. + + :param value: mode value + :return: mode + """ + for mode in cls: + if mode.value == value: + return mode + raise ValueError(f'invalid invoke from value {value}') + + def to_source(self) -> str: + """ + Get source of invoke from. + + :return: source + """ + if self == InvokeFrom.WEB_APP: + return 'web_app' + elif self == InvokeFrom.DEBUGGER: + return 'dev' + elif self == InvokeFrom.EXPLORE: + return 'explore_app' + elif self == InvokeFrom.SERVICE_API: + return 'api' + + return 'dev' + + +class EasyUIBasedModelConfigEntity(BaseModel): + """ + Model Config Entity. + """ + provider: str + model: str + model_schema: AIModelEntity + mode: str + provider_model_bundle: ProviderModelBundle + credentials: dict[str, Any] = {} + parameters: dict[str, Any] = {} + stop: list[str] = [] + + +class EasyUIBasedAppGenerateEntity(BaseModel): + """ + EasyUI Based Application Generate Entity. + """ + task_id: str + + # app config + app_config: EasyUIBasedAppConfig + model_config: EasyUIBasedModelConfigEntity + + conversation_id: Optional[str] = None + inputs: dict[str, str] + query: Optional[str] = None + files: list[FileObj] = [] + user_id: str + # extras + stream: bool + invoke_from: InvokeFrom + + # extra parameters, like: auto_generate_conversation_name + extras: dict[str, Any] = {} + + +class WorkflowUIBasedAppGenerateEntity(BaseModel): + """ + Workflow UI Based Application Generate Entity. + """ + task_id: str + + # app config + app_config: WorkflowUIBasedAppConfig + + inputs: dict[str, str] + files: list[FileObj] = [] + user_id: str + # extras + stream: bool + invoke_from: InvokeFrom + + # extra parameters + extras: dict[str, Any] = {} + + +class AdvancedChatAppGenerateEntity(WorkflowUIBasedAppGenerateEntity): + conversation_id: Optional[str] = None + query: str diff --git a/api/core/entities/queue_entities.py b/api/core/app/entities/queue_entities.py similarity index 100% rename from api/core/entities/queue_entities.py rename to api/core/app/entities/queue_entities.py diff --git a/api/core/app/features/annotation_reply/annotation_reply.py b/api/core/app/features/annotation_reply/annotation_reply.py index fd516e465f..19ff94de5e 100644 --- a/api/core/app/features/annotation_reply/annotation_reply.py +++ b/api/core/app/features/annotation_reply/annotation_reply.py @@ -1,7 +1,7 @@ import logging from typing import Optional -from core.entities.application_entities import InvokeFrom +from core.app.entities.app_invoke_entities import InvokeFrom from core.rag.datasource.vdb.vector_factory import Vector from extensions.ext_database import db from models.dataset import Dataset diff --git a/api/core/app/features/hosting_moderation/hosting_moderation.py b/api/core/app/features/hosting_moderation/hosting_moderation.py index d8ae7adcac..ec316248a2 100644 --- a/api/core/app/features/hosting_moderation/hosting_moderation.py +++ b/api/core/app/features/hosting_moderation/hosting_moderation.py @@ -1,6 +1,6 @@ import logging -from core.entities.application_entities import ApplicationGenerateEntity +from core.app.entities.app_invoke_entities import EasyUIBasedAppGenerateEntity from core.helper import moderation from core.model_runtime.entities.message_entities import PromptMessage @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) class HostingModerationFeature: - def check(self, application_generate_entity: ApplicationGenerateEntity, + def check(self, application_generate_entity: EasyUIBasedAppGenerateEntity, prompt_messages: list[PromptMessage]) -> bool: """ Check hosting moderation @@ -16,8 +16,7 @@ class HostingModerationFeature: :param prompt_messages: prompt messages :return: """ - app_orchestration_config = application_generate_entity.app_orchestration_config_entity - model_config = app_orchestration_config.model_config + model_config = application_generate_entity.model_config text = "" for prompt_message in prompt_messages: diff --git a/api/core/app/generate_task_pipeline.py b/api/core/app/generate_task_pipeline.py index dc6ea2db79..359369ef59 100644 --- a/api/core/app/generate_task_pipeline.py +++ b/api/core/app/generate_task_pipeline.py @@ -7,8 +7,8 @@ from typing import Optional, Union, cast from pydantic import BaseModel from core.app.app_queue_manager import AppQueueManager, PublishFrom -from core.entities.application_entities import ApplicationGenerateEntity, InvokeFrom -from core.entities.queue_entities import ( +from core.app.entities.app_invoke_entities import EasyUIBasedAppGenerateEntity, InvokeFrom +from core.app.entities.queue_entities import ( AnnotationReplyEvent, QueueAgentMessageEvent, QueueAgentThoughtEvent, @@ -58,7 +58,7 @@ class GenerateTaskPipeline: GenerateTaskPipeline is a class that generate stream output and state management for Application. """ - def __init__(self, application_generate_entity: ApplicationGenerateEntity, + def __init__(self, application_generate_entity: EasyUIBasedAppGenerateEntity, queue_manager: AppQueueManager, conversation: Conversation, message: Message) -> None: @@ -75,7 +75,7 @@ class GenerateTaskPipeline: self._message = message self._task_state = TaskState( llm_result=LLMResult( - model=self._application_generate_entity.app_orchestration_config_entity.model_config.model, + model=self._application_generate_entity.model_config.model, prompt_messages=[], message=AssistantPromptMessage(content=""), usage=LLMUsage.empty_usage() @@ -127,7 +127,7 @@ class GenerateTaskPipeline: if isinstance(event, QueueMessageEndEvent): self._task_state.llm_result = event.llm_result else: - model_config = self._application_generate_entity.app_orchestration_config_entity.model_config + model_config = self._application_generate_entity.model_config model = model_config.model model_type_instance = model_config.provider_model_bundle.model_type_instance model_type_instance = cast(LargeLanguageModel, model_type_instance) @@ -210,7 +210,7 @@ class GenerateTaskPipeline: if isinstance(event, QueueMessageEndEvent): self._task_state.llm_result = event.llm_result else: - model_config = self._application_generate_entity.app_orchestration_config_entity.model_config + model_config = self._application_generate_entity.model_config model = model_config.model model_type_instance = model_config.provider_model_bundle.model_type_instance model_type_instance = cast(LargeLanguageModel, model_type_instance) @@ -569,7 +569,7 @@ class GenerateTaskPipeline: :return: """ prompts = [] - if self._application_generate_entity.app_orchestration_config_entity.model_config.mode == 'chat': + if self._application_generate_entity.model_config.mode == 'chat': for prompt_message in prompt_messages: if prompt_message.role == PromptMessageRole.USER: role = 'user' @@ -638,13 +638,13 @@ class GenerateTaskPipeline: Init output moderation. :return: """ - app_orchestration_config_entity = self._application_generate_entity.app_orchestration_config_entity - sensitive_word_avoidance = app_orchestration_config_entity.sensitive_word_avoidance + app_config = self._application_generate_entity.app_config + sensitive_word_avoidance = app_config.sensitive_word_avoidance if sensitive_word_avoidance: return OutputModeration( - tenant_id=self._application_generate_entity.tenant_id, - app_id=self._application_generate_entity.app_id, + tenant_id=app_config.tenant_id, + app_id=app_config.app_id, rule=ModerationRule( type=sensitive_word_avoidance.type, config=sensitive_word_avoidance.config diff --git a/api/core/app/validators/external_data_fetch.py b/api/core/app/validators/external_data_fetch.py deleted file mode 100644 index 5910aa17e7..0000000000 --- a/api/core/app/validators/external_data_fetch.py +++ /dev/null @@ -1,39 +0,0 @@ - -from core.external_data_tool.factory import ExternalDataToolFactory - - -class ExternalDataFetchValidator: - @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/app/validators/user_input_form.py b/api/core/app/validators/user_input_form.py deleted file mode 100644 index 249d6745ae..0000000000 --- a/api/core/app/validators/user_input_form.py +++ /dev/null @@ -1,61 +0,0 @@ -import re - - -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/core/app/workflow/config_validator.py b/api/core/app/workflow/config_validator.py deleted file mode 100644 index e8381146a7..0000000000 --- a/api/core/app/workflow/config_validator.py +++ /dev/null @@ -1,39 +0,0 @@ -from core.app.validators.file_upload import FileUploadValidator -from core.app.validators.moderation import ModerationValidator -from core.app.validators.text_to_speech import TextToSpeechValidator - - -class WorkflowAppConfigValidator: - @classmethod - def config_validate(cls, tenant_id: str, config: dict, only_structure_validate: bool = False) -> dict: - """ - Validate for workflow app model config - - :param tenant_id: tenant id - :param config: app model config args - :param only_structure_validate: only validate the structure of the config - """ - 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=tenant_id, - config=config, - only_structure_validate=only_structure_validate - ) - 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/callback_handler/agent_loop_gather_callback_handler.py b/api/core/callback_handler/agent_loop_gather_callback_handler.py deleted file mode 100644 index 8a340a8b81..0000000000 --- a/api/core/callback_handler/agent_loop_gather_callback_handler.py +++ /dev/null @@ -1,262 +0,0 @@ -import json -import logging -import time -from typing import Any, Optional, Union, cast - -from langchain.agents import openai_functions_agent, openai_functions_multi_agent -from langchain.callbacks.base import BaseCallbackHandler -from langchain.schema import AgentAction, AgentFinish, BaseMessage, LLMResult - -from core.app.app_queue_manager import AppQueueManager, PublishFrom -from core.callback_handler.entity.agent_loop import AgentLoop -from core.entities.application_entities import ModelConfigEntity -from core.model_runtime.entities.llm_entities import LLMResult as RuntimeLLMResult -from core.model_runtime.entities.message_entities import AssistantPromptMessage, PromptMessage, UserPromptMessage -from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel -from extensions.ext_database import db -from models.model import Message, MessageAgentThought, MessageChain - - -class AgentLoopGatherCallbackHandler(BaseCallbackHandler): - """Callback Handler that prints to std out.""" - raise_error: bool = True - - def __init__(self, model_config: ModelConfigEntity, - queue_manager: AppQueueManager, - message: Message, - message_chain: MessageChain) -> None: - """Initialize callback handler.""" - self.model_config = model_config - self.queue_manager = queue_manager - self.message = message - self.message_chain = message_chain - model_type_instance = self.model_config.provider_model_bundle.model_type_instance - self.model_type_instance = cast(LargeLanguageModel, model_type_instance) - self._agent_loops = [] - self._current_loop = None - self._message_agent_thought = None - - @property - def agent_loops(self) -> list[AgentLoop]: - return self._agent_loops - - def clear_agent_loops(self) -> None: - self._agent_loops = [] - self._current_loop = None - self._message_agent_thought = None - - @property - def always_verbose(self) -> bool: - """Whether to call verbose callbacks even if verbose is False.""" - return True - - @property - def ignore_chain(self) -> bool: - """Whether to ignore chain callbacks.""" - return True - - def on_llm_before_invoke(self, prompt_messages: list[PromptMessage]) -> None: - if not self._current_loop: - # Agent start with a LLM query - self._current_loop = AgentLoop( - position=len(self._agent_loops) + 1, - prompt="\n".join([prompt_message.content for prompt_message in prompt_messages]), - status='llm_started', - started_at=time.perf_counter() - ) - - def on_llm_after_invoke(self, result: RuntimeLLMResult) -> None: - if self._current_loop and self._current_loop.status == 'llm_started': - self._current_loop.status = 'llm_end' - if result.usage: - self._current_loop.prompt_tokens = result.usage.prompt_tokens - else: - self._current_loop.prompt_tokens = self.model_type_instance.get_num_tokens( - model=self.model_config.model, - credentials=self.model_config.credentials, - prompt_messages=[UserPromptMessage(content=self._current_loop.prompt)] - ) - - completion_message = result.message - if completion_message.tool_calls: - self._current_loop.completion \ - = json.dumps({'function_call': completion_message.tool_calls}) - else: - self._current_loop.completion = completion_message.content - - if result.usage: - self._current_loop.completion_tokens = result.usage.completion_tokens - else: - self._current_loop.completion_tokens = self.model_type_instance.get_num_tokens( - model=self.model_config.model, - credentials=self.model_config.credentials, - prompt_messages=[AssistantPromptMessage(content=self._current_loop.completion)] - ) - - def on_chat_model_start( - self, - serialized: dict[str, Any], - messages: list[list[BaseMessage]], - **kwargs: Any - ) -> Any: - pass - - def on_llm_start( - self, serialized: dict[str, Any], prompts: list[str], **kwargs: Any - ) -> None: - pass - - def on_llm_end(self, response: LLMResult, **kwargs: Any) -> None: - """Do nothing.""" - pass - - def on_llm_error( - self, error: Union[Exception, KeyboardInterrupt], **kwargs: Any - ) -> None: - logging.debug("Agent on_llm_error: %s", error) - self._agent_loops = [] - self._current_loop = None - self._message_agent_thought = None - - def on_tool_start( - self, - serialized: dict[str, Any], - input_str: str, - **kwargs: Any, - ) -> None: - """Do nothing.""" - # kwargs={'color': 'green', 'llm_prefix': 'Thought:', 'observation_prefix': 'Observation: '} - # input_str='action-input' - # serialized={'description': 'A search engine. Useful for when you need to answer questions about current events. Input should be a search query.', 'name': 'Search'} - pass - - def on_agent_action( - self, action: AgentAction, color: Optional[str] = None, **kwargs: Any - ) -> Any: - """Run on agent action.""" - tool = action.tool - tool_input = json.dumps({"query": action.tool_input} - if isinstance(action.tool_input, str) else action.tool_input) - completion = None - if isinstance(action, openai_functions_agent.base._FunctionsAgentAction) \ - or isinstance(action, openai_functions_multi_agent.base._FunctionsAgentAction): - thought = action.log.strip() - completion = json.dumps({'function_call': action.message_log[0].additional_kwargs['function_call']}) - else: - action_name_position = action.log.index("Action:") if action.log else -1 - thought = action.log[:action_name_position].strip() if action.log else '' - - if self._current_loop and self._current_loop.status == 'llm_end': - self._current_loop.status = 'agent_action' - self._current_loop.thought = thought - self._current_loop.tool_name = tool - self._current_loop.tool_input = tool_input - if completion is not None: - self._current_loop.completion = completion - - self._message_agent_thought = self._init_agent_thought() - - def on_tool_end( - self, - output: str, - color: Optional[str] = None, - observation_prefix: Optional[str] = None, - llm_prefix: Optional[str] = None, - **kwargs: Any, - ) -> None: - """If not the final action, print out observation.""" - # kwargs={'name': 'Search'} - # llm_prefix='Thought:' - # observation_prefix='Observation: ' - # output='53 years' - - if self._current_loop and self._current_loop.status == 'agent_action' and output and output != 'None': - self._current_loop.status = 'tool_end' - self._current_loop.tool_output = output - self._current_loop.completed = True - self._current_loop.completed_at = time.perf_counter() - self._current_loop.latency = self._current_loop.completed_at - self._current_loop.started_at - - self._complete_agent_thought(self._message_agent_thought) - - self._agent_loops.append(self._current_loop) - self._current_loop = None - self._message_agent_thought = None - - def on_tool_error( - self, error: Union[Exception, KeyboardInterrupt], **kwargs: Any - ) -> None: - """Do nothing.""" - logging.debug("Agent on_tool_error: %s", error) - self._agent_loops = [] - self._current_loop = None - self._message_agent_thought = None - - def on_agent_finish(self, finish: AgentFinish, **kwargs: Any) -> Any: - """Run on agent end.""" - # Final Answer - if self._current_loop and (self._current_loop.status == 'llm_end' or self._current_loop.status == 'agent_action'): - self._current_loop.status = 'agent_finish' - self._current_loop.completed = True - self._current_loop.completed_at = time.perf_counter() - self._current_loop.latency = self._current_loop.completed_at - self._current_loop.started_at - self._current_loop.thought = '[DONE]' - self._message_agent_thought = self._init_agent_thought() - - self._complete_agent_thought(self._message_agent_thought) - - self._agent_loops.append(self._current_loop) - self._current_loop = None - self._message_agent_thought = None - elif not self._current_loop and self._agent_loops: - self._agent_loops[-1].status = 'agent_finish' - - def _init_agent_thought(self) -> MessageAgentThought: - message_agent_thought = MessageAgentThought( - message_id=self.message.id, - message_chain_id=self.message_chain.id, - position=self._current_loop.position, - thought=self._current_loop.thought, - tool=self._current_loop.tool_name, - tool_input=self._current_loop.tool_input, - message=self._current_loop.prompt, - message_price_unit=0, - answer=self._current_loop.completion, - answer_price_unit=0, - created_by_role=('account' if self.message.from_source == 'console' else 'end_user'), - created_by=(self.message.from_account_id - if self.message.from_source == 'console' else self.message.from_end_user_id) - ) - - db.session.add(message_agent_thought) - db.session.commit() - - self.queue_manager.publish_agent_thought(message_agent_thought, PublishFrom.APPLICATION_MANAGER) - - return message_agent_thought - - def _complete_agent_thought(self, message_agent_thought: MessageAgentThought) -> None: - loop_message_tokens = self._current_loop.prompt_tokens - loop_answer_tokens = self._current_loop.completion_tokens - - # transform usage - llm_usage = self.model_type_instance._calc_response_usage( - self.model_config.model, - self.model_config.credentials, - loop_message_tokens, - loop_answer_tokens - ) - - message_agent_thought.observation = self._current_loop.tool_output - message_agent_thought.tool_process_data = '' # currently not support - message_agent_thought.message_token = loop_message_tokens - message_agent_thought.message_unit_price = llm_usage.prompt_unit_price - message_agent_thought.message_price_unit = llm_usage.prompt_price_unit - message_agent_thought.answer_token = loop_answer_tokens - message_agent_thought.answer_unit_price = llm_usage.completion_unit_price - message_agent_thought.answer_price_unit = llm_usage.completion_price_unit - message_agent_thought.latency = self._current_loop.latency - message_agent_thought.tokens = self._current_loop.prompt_tokens + self._current_loop.completion_tokens - message_agent_thought.total_price = llm_usage.total_price - message_agent_thought.currency = llm_usage.currency - db.session.commit() diff --git a/api/core/callback_handler/entity/agent_loop.py b/api/core/callback_handler/entity/agent_loop.py deleted file mode 100644 index 56634bb19e..0000000000 --- a/api/core/callback_handler/entity/agent_loop.py +++ /dev/null @@ -1,23 +0,0 @@ -from pydantic import BaseModel - - -class AgentLoop(BaseModel): - position: int = 1 - - thought: str = None - tool_name: str = None - tool_input: str = None - tool_output: str = None - - prompt: str = None - prompt_tokens: int = 0 - completion: str = None - completion_tokens: int = 0 - - latency: float = None - - status: str = 'llm_started' - completed: bool = False - - started_at: float = None - completed_at: float = None \ No newline at end of file diff --git a/api/core/callback_handler/index_tool_callback_handler.py b/api/core/callback_handler/index_tool_callback_handler.py index e49a09d4c4..ca781a55bc 100644 --- a/api/core/callback_handler/index_tool_callback_handler.py +++ b/api/core/callback_handler/index_tool_callback_handler.py @@ -1,6 +1,6 @@ from core.app.app_queue_manager import AppQueueManager, PublishFrom -from core.entities.application_entities import InvokeFrom +from core.app.entities.app_invoke_entities import InvokeFrom from core.rag.models.document import Document from extensions.ext_database import db from models.dataset import DatasetQuery, DocumentSegment diff --git a/api/core/external_data_tool/external_data_fetch.py b/api/core/external_data_tool/external_data_fetch.py index 64c7d1e859..8601cb34e7 100644 --- a/api/core/external_data_tool/external_data_fetch.py +++ b/api/core/external_data_tool/external_data_fetch.py @@ -5,7 +5,7 @@ from typing import Optional from flask import Flask, current_app -from core.entities.application_entities import ExternalDataVariableEntity +from core.app.app_config.entities import ExternalDataVariableEntity from core.external_data_tool.factory import ExternalDataToolFactory logger = logging.getLogger(__name__) diff --git a/api/core/file/file_obj.py b/api/core/file/file_obj.py index 435074f743..bd896719c2 100644 --- a/api/core/file/file_obj.py +++ b/api/core/file/file_obj.py @@ -3,6 +3,7 @@ from typing import Optional from pydantic import BaseModel +from core.app.app_config.entities import FileUploadEntity from core.file.upload_file_parser import UploadFileParser from core.model_runtime.entities.message_entities import ImagePromptMessageContent from extensions.ext_database import db @@ -50,7 +51,7 @@ class FileObj(BaseModel): transfer_method: FileTransferMethod url: Optional[str] upload_file_id: Optional[str] - file_config: dict + file_upload_entity: FileUploadEntity @property def data(self) -> Optional[str]: @@ -63,7 +64,7 @@ class FileObj(BaseModel): @property def prompt_message_content(self) -> ImagePromptMessageContent: if self.type == FileType.IMAGE: - image_config = self.file_config.get('image') + image_config = self.file_upload_entity.image_config return ImagePromptMessageContent( data=self.data, diff --git a/api/core/file/message_file_parser.py b/api/core/file/message_file_parser.py index c132073578..9d122c4120 100644 --- a/api/core/file/message_file_parser.py +++ b/api/core/file/message_file_parser.py @@ -1,11 +1,12 @@ -from typing import Optional, Union +from typing import Union import requests +from core.app.app_config.entities import FileUploadEntity from core.file.file_obj import FileBelongsTo, FileObj, FileTransferMethod, FileType from extensions.ext_database import db from models.account import Account -from models.model import AppModelConfig, EndUser, MessageFile, UploadFile +from models.model import EndUser, MessageFile, UploadFile from services.file_service import IMAGE_EXTENSIONS @@ -15,18 +16,16 @@ class MessageFileParser: self.tenant_id = tenant_id self.app_id = app_id - def validate_and_transform_files_arg(self, files: list[dict], app_model_config: AppModelConfig, + def validate_and_transform_files_arg(self, files: list[dict], file_upload_entity: FileUploadEntity, user: Union[Account, EndUser]) -> list[FileObj]: """ validate and transform files arg :param files: - :param app_model_config: + :param file_upload_entity: :param user: :return: """ - file_upload_config = app_model_config.file_upload_dict - for file in files: if not isinstance(file, dict): raise ValueError('Invalid file format, must be dict') @@ -45,17 +44,17 @@ class MessageFileParser: raise ValueError('Missing file upload_file_id') # transform files to file objs - type_file_objs = self._to_file_objs(files, file_upload_config) + type_file_objs = self._to_file_objs(files, file_upload_entity) # validate files new_files = [] for file_type, file_objs in type_file_objs.items(): if file_type == FileType.IMAGE: # parse and validate files - image_config = file_upload_config.get('image') + image_config = file_upload_entity.image_config # check if image file feature is enabled - if not image_config['enabled']: + if not image_config: continue # Validate number of files @@ -96,27 +95,27 @@ class MessageFileParser: # return all file objs return new_files - def transform_message_files(self, files: list[MessageFile], file_upload_config: Optional[dict]) -> list[FileObj]: + def transform_message_files(self, files: list[MessageFile], file_upload_entity: FileUploadEntity) -> list[FileObj]: """ transform message files :param files: - :param file_upload_config: + :param file_upload_entity: :return: """ # transform files to file objs - type_file_objs = self._to_file_objs(files, file_upload_config) + type_file_objs = self._to_file_objs(files, file_upload_entity) # return all file objs return [file_obj for file_objs in type_file_objs.values() for file_obj in file_objs] def _to_file_objs(self, files: list[Union[dict, MessageFile]], - file_upload_config: dict) -> dict[FileType, list[FileObj]]: + file_upload_entity: FileUploadEntity) -> dict[FileType, list[FileObj]]: """ transform files to file objs :param files: - :param file_upload_config: + :param file_upload_entity: :return: """ type_file_objs: dict[FileType, list[FileObj]] = { @@ -133,7 +132,7 @@ class MessageFileParser: if file.belongs_to == FileBelongsTo.ASSISTANT.value: continue - file_obj = self._to_file_obj(file, file_upload_config) + file_obj = self._to_file_obj(file, file_upload_entity) if file_obj.type not in type_file_objs: continue @@ -141,7 +140,7 @@ class MessageFileParser: return type_file_objs - def _to_file_obj(self, file: Union[dict, MessageFile], file_upload_config: dict) -> FileObj: + def _to_file_obj(self, file: Union[dict, MessageFile], file_upload_entity: FileUploadEntity) -> FileObj: """ transform file to file obj @@ -156,7 +155,7 @@ class MessageFileParser: transfer_method=transfer_method, url=file.get('url') if transfer_method == FileTransferMethod.REMOTE_URL else None, upload_file_id=file.get('upload_file_id') if transfer_method == FileTransferMethod.LOCAL_FILE else None, - file_config=file_upload_config + file_upload_entity=file_upload_entity ) else: return FileObj( @@ -166,7 +165,7 @@ class MessageFileParser: transfer_method=FileTransferMethod.value_of(file.transfer_method), url=file.url, upload_file_id=file.upload_file_id or None, - file_config=file_upload_config + file_upload_entity=file_upload_entity ) def _check_image_remote_url(self, url): diff --git a/api/core/helper/moderation.py b/api/core/helper/moderation.py index 86d6b498da..bff9b9cf1f 100644 --- a/api/core/helper/moderation.py +++ b/api/core/helper/moderation.py @@ -1,7 +1,7 @@ import logging import random -from core.entities.application_entities import ModelConfigEntity +from core.app.entities.app_invoke_entities import EasyUIBasedModelConfigEntity from core.model_runtime.errors.invoke import InvokeBadRequestError from core.model_runtime.model_providers.openai.moderation.moderation import OpenAIModerationModel from extensions.ext_hosting_provider import hosting_configuration @@ -10,7 +10,7 @@ from models.provider import ProviderType logger = logging.getLogger(__name__) -def check_moderation(model_config: ModelConfigEntity, text: str) -> bool: +def check_moderation(model_config: EasyUIBasedModelConfigEntity, text: str) -> bool: moderation_config = hosting_configuration.moderation_config if (moderation_config and moderation_config.enabled is True and 'openai' in hosting_configuration.provider_map diff --git a/api/core/memory/token_buffer_memory.py b/api/core/memory/token_buffer_memory.py index 00813faef7..4fe150e983 100644 --- a/api/core/memory/token_buffer_memory.py +++ b/api/core/memory/token_buffer_memory.py @@ -1,3 +1,5 @@ +from core.app.app_config.entities import FileUploadEntity +from core.app.app_config.features.file_upload.manager import FileUploadConfigManager from core.file.message_file_parser import MessageFileParser from core.model_manager import ModelInstance from core.model_runtime.entities.message_entities import ( @@ -43,12 +45,18 @@ class TokenBufferMemory: for message in messages: files = message.message_files if files: - file_objs = message_file_parser.transform_message_files( - files, - message.app_model_config.file_upload_dict - if self.conversation.mode not in [AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value] - else message.workflow_run.workflow.features_dict.get('file_upload', {}) - ) + if self.conversation.mode not in [AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value]: + file_upload_entity = FileUploadConfigManager.convert(message.app_model_config.to_dict()) + else: + file_upload_entity = FileUploadConfigManager.convert(message.workflow_run.workflow.features_dict) + + if file_upload_entity: + file_objs = message_file_parser.transform_message_files( + files, + file_upload_entity + ) + else: + file_objs = [] if not file_objs: prompt_messages.append(UserPromptMessage(content=message.query)) diff --git a/api/core/moderation/input_moderation.py b/api/core/moderation/input_moderation.py index 2129c58d8d..8fbc0c2d50 100644 --- a/api/core/moderation/input_moderation.py +++ b/api/core/moderation/input_moderation.py @@ -1,6 +1,6 @@ import logging -from core.entities.application_entities import AppOrchestrationConfigEntity +from core.app.app_config.entities import AppConfig from core.moderation.base import ModerationAction, ModerationException from core.moderation.factory import ModerationFactory @@ -10,22 +10,22 @@ logger = logging.getLogger(__name__) class InputModeration: def check(self, app_id: str, tenant_id: str, - app_orchestration_config_entity: AppOrchestrationConfigEntity, + app_config: AppConfig, inputs: dict, query: str) -> tuple[bool, dict, str]: """ Process sensitive_word_avoidance. :param app_id: app id :param tenant_id: tenant id - :param app_orchestration_config_entity: app orchestration config entity + :param app_config: app config :param inputs: inputs :param query: query :return: """ - if not app_orchestration_config_entity.sensitive_word_avoidance: + if not app_config.sensitive_word_avoidance: return False, inputs, query - sensitive_word_avoidance_config = app_orchestration_config_entity.sensitive_word_avoidance + sensitive_word_avoidance_config = app_config.sensitive_word_avoidance moderation_type = sensitive_word_avoidance_config.type moderation_factory = ModerationFactory( diff --git a/api/core/prompt/advanced_prompt_transform.py b/api/core/prompt/advanced_prompt_transform.py index 6d0a1d31f5..129c2a4cd2 100644 --- a/api/core/prompt/advanced_prompt_transform.py +++ b/api/core/prompt/advanced_prompt_transform.py @@ -1,10 +1,7 @@ from typing import Optional -from core.entities.application_entities import ( - AdvancedCompletionPromptTemplateEntity, - ModelConfigEntity, - PromptTemplateEntity, -) +from core.app.app_config.entities import PromptTemplateEntity, AdvancedCompletionPromptTemplateEntity +from core.app.entities.app_invoke_entities import EasyUIBasedModelConfigEntity from core.file.file_obj import FileObj from core.memory.token_buffer_memory import TokenBufferMemory from core.model_runtime.entities.message_entities import ( @@ -31,7 +28,7 @@ class AdvancedPromptTransform(PromptTransform): files: list[FileObj], context: Optional[str], memory: Optional[TokenBufferMemory], - model_config: ModelConfigEntity) -> list[PromptMessage]: + model_config: EasyUIBasedModelConfigEntity) -> list[PromptMessage]: prompt_messages = [] model_mode = ModelMode.value_of(model_config.mode) @@ -65,7 +62,7 @@ class AdvancedPromptTransform(PromptTransform): files: list[FileObj], context: Optional[str], memory: Optional[TokenBufferMemory], - model_config: ModelConfigEntity) -> list[PromptMessage]: + model_config: EasyUIBasedModelConfigEntity) -> list[PromptMessage]: """ Get completion model prompt messages. """ @@ -113,7 +110,7 @@ class AdvancedPromptTransform(PromptTransform): files: list[FileObj], context: Optional[str], memory: Optional[TokenBufferMemory], - model_config: ModelConfigEntity) -> list[PromptMessage]: + model_config: EasyUIBasedModelConfigEntity) -> list[PromptMessage]: """ Get chat model prompt messages. """ @@ -202,7 +199,7 @@ class AdvancedPromptTransform(PromptTransform): role_prefix: AdvancedCompletionPromptTemplateEntity.RolePrefixEntity, prompt_template: PromptTemplateParser, prompt_inputs: dict, - model_config: ModelConfigEntity) -> dict: + model_config: EasyUIBasedModelConfigEntity) -> dict: if '#histories#' in prompt_template.variable_keys: if memory: inputs = {'#histories#': '', **prompt_inputs} diff --git a/api/core/prompt/prompt_transform.py b/api/core/prompt/prompt_transform.py index 9c554140b7..7fe8128a49 100644 --- a/api/core/prompt/prompt_transform.py +++ b/api/core/prompt/prompt_transform.py @@ -1,6 +1,6 @@ from typing import Optional, cast -from core.entities.application_entities import ModelConfigEntity +from core.app.entities.app_invoke_entities import EasyUIBasedModelConfigEntity from core.memory.token_buffer_memory import TokenBufferMemory from core.model_runtime.entities.message_entities import PromptMessage from core.model_runtime.entities.model_entities import ModelPropertyKey @@ -10,14 +10,14 @@ from core.model_runtime.model_providers.__base.large_language_model import Large class PromptTransform: def _append_chat_histories(self, memory: TokenBufferMemory, prompt_messages: list[PromptMessage], - model_config: ModelConfigEntity) -> list[PromptMessage]: + model_config: EasyUIBasedModelConfigEntity) -> list[PromptMessage]: rest_tokens = self._calculate_rest_token(prompt_messages, model_config) histories = self._get_history_messages_list_from_memory(memory, rest_tokens) prompt_messages.extend(histories) return prompt_messages - def _calculate_rest_token(self, prompt_messages: list[PromptMessage], model_config: ModelConfigEntity) -> int: + def _calculate_rest_token(self, prompt_messages: list[PromptMessage], model_config: EasyUIBasedModelConfigEntity) -> int: rest_tokens = 2000 model_context_tokens = model_config.model_schema.model_properties.get(ModelPropertyKey.CONTEXT_SIZE) diff --git a/api/core/prompt/simple_prompt_transform.py b/api/core/prompt/simple_prompt_transform.py index af7b695bb3..faf1f888e2 100644 --- a/api/core/prompt/simple_prompt_transform.py +++ b/api/core/prompt/simple_prompt_transform.py @@ -3,10 +3,8 @@ import json import os from typing import Optional -from core.entities.application_entities import ( - ModelConfigEntity, - PromptTemplateEntity, -) +from core.app.app_config.entities import PromptTemplateEntity +from core.app.entities.app_invoke_entities import EasyUIBasedModelConfigEntity from core.file.file_obj import FileObj from core.memory.token_buffer_memory import TokenBufferMemory from core.model_runtime.entities.message_entities import ( @@ -54,7 +52,7 @@ class SimplePromptTransform(PromptTransform): files: list[FileObj], context: Optional[str], memory: Optional[TokenBufferMemory], - model_config: ModelConfigEntity) -> \ + model_config: EasyUIBasedModelConfigEntity) -> \ tuple[list[PromptMessage], Optional[list[str]]]: model_mode = ModelMode.value_of(model_config.mode) if model_mode == ModelMode.CHAT: @@ -83,7 +81,7 @@ class SimplePromptTransform(PromptTransform): return prompt_messages, stops def get_prompt_str_and_rules(self, app_mode: AppMode, - model_config: ModelConfigEntity, + model_config: EasyUIBasedModelConfigEntity, pre_prompt: str, inputs: dict, query: Optional[str] = None, @@ -164,7 +162,7 @@ class SimplePromptTransform(PromptTransform): context: Optional[str], files: list[FileObj], memory: Optional[TokenBufferMemory], - model_config: ModelConfigEntity) \ + model_config: EasyUIBasedModelConfigEntity) \ -> tuple[list[PromptMessage], Optional[list[str]]]: prompt_messages = [] @@ -202,7 +200,7 @@ class SimplePromptTransform(PromptTransform): context: Optional[str], files: list[FileObj], memory: Optional[TokenBufferMemory], - model_config: ModelConfigEntity) \ + model_config: EasyUIBasedModelConfigEntity) \ -> tuple[list[PromptMessage], Optional[list[str]]]: # get prompt prompt, prompt_rules = self.get_prompt_str_and_rules( diff --git a/api/core/rag/retrieval/agent/agent_llm_callback.py b/api/core/rag/retrieval/agent/agent_llm_callback.py deleted file mode 100644 index 5ec549de8e..0000000000 --- a/api/core/rag/retrieval/agent/agent_llm_callback.py +++ /dev/null @@ -1,101 +0,0 @@ -import logging -from typing import Optional - -from core.callback_handler.agent_loop_gather_callback_handler import AgentLoopGatherCallbackHandler -from core.model_runtime.callbacks.base_callback import Callback -from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk -from core.model_runtime.entities.message_entities import PromptMessage, PromptMessageTool -from core.model_runtime.model_providers.__base.ai_model import AIModel - -logger = logging.getLogger(__name__) - - -class AgentLLMCallback(Callback): - - def __init__(self, agent_callback: AgentLoopGatherCallbackHandler) -> None: - self.agent_callback = agent_callback - - def on_before_invoke(self, llm_instance: AIModel, model: str, credentials: dict, - prompt_messages: list[PromptMessage], model_parameters: dict, - tools: Optional[list[PromptMessageTool]] = None, stop: Optional[list[str]] = None, - stream: bool = True, user: Optional[str] = None) -> None: - """ - Before invoke callback - - :param llm_instance: LLM instance - :param model: model name - :param credentials: model credentials - :param prompt_messages: prompt messages - :param model_parameters: model parameters - :param tools: tools for tool calling - :param stop: stop words - :param stream: is stream response - :param user: unique user id - """ - self.agent_callback.on_llm_before_invoke( - prompt_messages=prompt_messages - ) - - def on_new_chunk(self, llm_instance: AIModel, chunk: LLMResultChunk, model: str, credentials: dict, - prompt_messages: list[PromptMessage], model_parameters: dict, - tools: Optional[list[PromptMessageTool]] = None, stop: Optional[list[str]] = None, - stream: bool = True, user: Optional[str] = None): - """ - On new chunk callback - - :param llm_instance: LLM instance - :param chunk: chunk - :param model: model name - :param credentials: model credentials - :param prompt_messages: prompt messages - :param model_parameters: model parameters - :param tools: tools for tool calling - :param stop: stop words - :param stream: is stream response - :param user: unique user id - """ - pass - - def on_after_invoke(self, llm_instance: AIModel, result: LLMResult, model: str, credentials: dict, - prompt_messages: list[PromptMessage], model_parameters: dict, - tools: Optional[list[PromptMessageTool]] = None, stop: Optional[list[str]] = None, - stream: bool = True, user: Optional[str] = None) -> None: - """ - After invoke callback - - :param llm_instance: LLM instance - :param result: result - :param model: model name - :param credentials: model credentials - :param prompt_messages: prompt messages - :param model_parameters: model parameters - :param tools: tools for tool calling - :param stop: stop words - :param stream: is stream response - :param user: unique user id - """ - self.agent_callback.on_llm_after_invoke( - result=result - ) - - def on_invoke_error(self, llm_instance: AIModel, ex: Exception, model: str, credentials: dict, - prompt_messages: list[PromptMessage], model_parameters: dict, - tools: Optional[list[PromptMessageTool]] = None, stop: Optional[list[str]] = None, - stream: bool = True, user: Optional[str] = None) -> None: - """ - Invoke error callback - - :param llm_instance: LLM instance - :param ex: exception - :param model: model name - :param credentials: model credentials - :param prompt_messages: prompt messages - :param model_parameters: model parameters - :param tools: tools for tool calling - :param stop: stop words - :param stream: is stream response - :param user: unique user id - """ - self.agent_callback.on_llm_error( - error=ex - ) diff --git a/api/core/rag/retrieval/agent/llm_chain.py b/api/core/rag/retrieval/agent/llm_chain.py index 087b7bfa2c..9b115bc696 100644 --- a/api/core/rag/retrieval/agent/llm_chain.py +++ b/api/core/rag/retrieval/agent/llm_chain.py @@ -5,19 +5,17 @@ from langchain.callbacks.manager import CallbackManagerForChainRun from langchain.schema import Generation, LLMResult from langchain.schema.language_model import BaseLanguageModel -from core.entities.application_entities import ModelConfigEntity +from core.app.entities.app_invoke_entities import EasyUIBasedModelConfigEntity from core.entities.message_entities import lc_messages_to_prompt_messages from core.model_manager import ModelInstance -from core.rag.retrieval.agent.agent_llm_callback import AgentLLMCallback from core.rag.retrieval.agent.fake_llm import FakeLLM class LLMChain(LCLLMChain): - model_config: ModelConfigEntity + model_config: EasyUIBasedModelConfigEntity """The language model instance to use.""" llm: BaseLanguageModel = FakeLLM(response="") parameters: dict[str, Any] = {} - agent_llm_callback: Optional[AgentLLMCallback] = None def generate( self, @@ -38,7 +36,6 @@ class LLMChain(LCLLMChain): prompt_messages=prompt_messages, stream=False, stop=stop, - callbacks=[self.agent_llm_callback] if self.agent_llm_callback else None, model_parameters=self.parameters ) diff --git a/api/core/rag/retrieval/agent/multi_dataset_router_agent.py b/api/core/rag/retrieval/agent/multi_dataset_router_agent.py index 41a0c54041..84e2b0228f 100644 --- a/api/core/rag/retrieval/agent/multi_dataset_router_agent.py +++ b/api/core/rag/retrieval/agent/multi_dataset_router_agent.py @@ -10,7 +10,7 @@ from langchain.schema import AgentAction, AgentFinish, AIMessage, SystemMessage from langchain.tools import BaseTool from pydantic import root_validator -from core.entities.application_entities import ModelConfigEntity +from core.app.entities.app_invoke_entities import EasyUIBasedModelConfigEntity from core.entities.message_entities import lc_messages_to_prompt_messages from core.model_manager import ModelInstance from core.model_runtime.entities.message_entities import PromptMessageTool @@ -21,7 +21,7 @@ class MultiDatasetRouterAgent(OpenAIFunctionsAgent): """ An Multi Dataset Retrieve Agent driven by Router. """ - model_config: ModelConfigEntity + model_config: EasyUIBasedModelConfigEntity class Config: """Configuration for this pydantic object.""" @@ -156,7 +156,7 @@ class MultiDatasetRouterAgent(OpenAIFunctionsAgent): @classmethod def from_llm_and_tools( cls, - model_config: ModelConfigEntity, + model_config: EasyUIBasedModelConfigEntity, tools: Sequence[BaseTool], callback_manager: Optional[BaseCallbackManager] = None, extra_prompt_messages: Optional[list[BaseMessagePromptTemplate]] = None, diff --git a/api/core/rag/retrieval/agent/structed_multi_dataset_router_agent.py b/api/core/rag/retrieval/agent/structed_multi_dataset_router_agent.py index 4d7d33038b..700bf0c293 100644 --- a/api/core/rag/retrieval/agent/structed_multi_dataset_router_agent.py +++ b/api/core/rag/retrieval/agent/structed_multi_dataset_router_agent.py @@ -12,7 +12,7 @@ from langchain.prompts import ChatPromptTemplate, HumanMessagePromptTemplate, Sy from langchain.schema import AgentAction, AgentFinish, OutputParserException from langchain.tools import BaseTool -from core.entities.application_entities import ModelConfigEntity +from core.app.entities.app_invoke_entities import EasyUIBasedModelConfigEntity from core.rag.retrieval.agent.llm_chain import LLMChain FORMAT_INSTRUCTIONS = """Use a json blob to specify a tool by providing an action key (tool name) and an action_input key (tool input). @@ -206,7 +206,7 @@ Thought: {agent_scratchpad} @classmethod def from_llm_and_tools( cls, - model_config: ModelConfigEntity, + model_config: EasyUIBasedModelConfigEntity, tools: Sequence[BaseTool], callback_manager: Optional[BaseCallbackManager] = None, output_parser: Optional[AgentOutputParser] = None, diff --git a/api/core/rag/retrieval/agent_based_dataset_executor.py b/api/core/rag/retrieval/agent_based_dataset_executor.py index 7fabf71bed..749e603c5c 100644 --- a/api/core/rag/retrieval/agent_based_dataset_executor.py +++ b/api/core/rag/retrieval/agent_based_dataset_executor.py @@ -7,13 +7,12 @@ from langchain.callbacks.manager import Callbacks from langchain.tools import BaseTool from pydantic import BaseModel, Extra +from core.app.entities.app_invoke_entities import EasyUIBasedModelConfigEntity from core.entities.agent_entities import PlanningStrategy -from core.entities.application_entities import ModelConfigEntity from core.entities.message_entities import prompt_messages_to_lc_messages from core.helper import moderation from core.memory.token_buffer_memory import TokenBufferMemory from core.model_runtime.errors.invoke import InvokeError -from core.rag.retrieval.agent.agent_llm_callback import AgentLLMCallback from core.rag.retrieval.agent.multi_dataset_router_agent import MultiDatasetRouterAgent from core.rag.retrieval.agent.output_parser.structured_chat import StructuredChatOutputParser from core.rag.retrieval.agent.structed_multi_dataset_router_agent import StructuredMultiDatasetRouterAgent @@ -23,15 +22,14 @@ from core.tools.tool.dataset_retriever.dataset_retriever_tool import DatasetRetr class AgentConfiguration(BaseModel): strategy: PlanningStrategy - model_config: ModelConfigEntity + model_config: EasyUIBasedModelConfigEntity tools: list[BaseTool] - summary_model_config: Optional[ModelConfigEntity] = None + summary_model_config: Optional[EasyUIBasedModelConfigEntity] = None memory: Optional[TokenBufferMemory] = None callbacks: Callbacks = None max_iterations: int = 6 max_execution_time: Optional[float] = None early_stopping_method: str = "generate" - agent_llm_callback: Optional[AgentLLMCallback] = None # `generate` will continue to complete the last inference after reaching the iteration limit or request time limit class Config: diff --git a/api/core/rag/retrieval/dataset_retrieval.py b/api/core/rag/retrieval/dataset_retrieval.py index 21e16c4162..8f1221adc7 100644 --- a/api/core/rag/retrieval/dataset_retrieval.py +++ b/api/core/rag/retrieval/dataset_retrieval.py @@ -2,9 +2,10 @@ from typing import Optional, cast from langchain.tools import BaseTool +from core.app.app_config.entities import DatasetEntity, DatasetRetrieveConfigEntity from core.callback_handler.index_tool_callback_handler import DatasetIndexToolCallbackHandler from core.entities.agent_entities import PlanningStrategy -from core.entities.application_entities import DatasetEntity, DatasetRetrieveConfigEntity, InvokeFrom, ModelConfigEntity +from core.app.entities.app_invoke_entities import InvokeFrom, EasyUIBasedModelConfigEntity from core.memory.token_buffer_memory import TokenBufferMemory from core.model_runtime.entities.model_entities import ModelFeature from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel @@ -17,7 +18,7 @@ from models.dataset import Dataset class DatasetRetrieval: def retrieve(self, tenant_id: str, - model_config: ModelConfigEntity, + model_config: EasyUIBasedModelConfigEntity, config: DatasetEntity, query: str, invoke_from: InvokeFrom, diff --git a/api/core/tools/tool/dataset_retriever_tool.py b/api/core/tools/tool/dataset_retriever_tool.py index 629ed23613..80062e606a 100644 --- a/api/core/tools/tool/dataset_retriever_tool.py +++ b/api/core/tools/tool/dataset_retriever_tool.py @@ -2,8 +2,9 @@ from typing import Any from langchain.tools import BaseTool +from core.app.app_config.entities import DatasetRetrieveConfigEntity from core.callback_handler.index_tool_callback_handler import DatasetIndexToolCallbackHandler -from core.entities.application_entities import DatasetRetrieveConfigEntity, InvokeFrom +from core.app.entities.app_invoke_entities import InvokeFrom from core.rag.retrieval.dataset_retrieval import DatasetRetrieval from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_entities import ToolDescription, ToolIdentity, ToolInvokeMessage, ToolParameter diff --git a/api/events/event_handlers/deduct_quota_when_messaeg_created.py b/api/events/event_handlers/deduct_quota_when_messaeg_created.py index 8c335f201f..49eea603dc 100644 --- a/api/events/event_handlers/deduct_quota_when_messaeg_created.py +++ b/api/events/event_handlers/deduct_quota_when_messaeg_created.py @@ -1,4 +1,4 @@ -from core.entities.application_entities import ApplicationGenerateEntity +from core.app.entities.app_invoke_entities import EasyUIBasedAppGenerateEntity from core.entities.provider_entities import QuotaUnit from events.message_event import message_was_created from extensions.ext_database import db @@ -8,9 +8,9 @@ from models.provider import Provider, ProviderType @message_was_created.connect def handle(sender, **kwargs): message = sender - application_generate_entity: ApplicationGenerateEntity = kwargs.get('application_generate_entity') + application_generate_entity: EasyUIBasedAppGenerateEntity = kwargs.get('application_generate_entity') - model_config = application_generate_entity.app_orchestration_config_entity.model_config + model_config = application_generate_entity.model_config provider_model_bundle = model_config.provider_model_bundle provider_configuration = provider_model_bundle.configuration @@ -43,7 +43,7 @@ def handle(sender, **kwargs): if used_quota is not None: db.session.query(Provider).filter( - Provider.tenant_id == application_generate_entity.tenant_id, + Provider.tenant_id == application_generate_entity.app_config.tenant_id, Provider.provider_name == model_config.provider, Provider.provider_type == ProviderType.SYSTEM.value, Provider.quota_type == system_configuration.current_quota_type.value, diff --git a/api/events/event_handlers/update_provider_last_used_at_when_messaeg_created.py b/api/events/event_handlers/update_provider_last_used_at_when_messaeg_created.py index 69b3a90e44..d49e560a67 100644 --- a/api/events/event_handlers/update_provider_last_used_at_when_messaeg_created.py +++ b/api/events/event_handlers/update_provider_last_used_at_when_messaeg_created.py @@ -1,6 +1,6 @@ from datetime import datetime -from core.entities.application_entities import ApplicationGenerateEntity +from core.app.entities.app_invoke_entities import EasyUIBasedAppGenerateEntity from events.message_event import message_was_created from extensions.ext_database import db from models.provider import Provider @@ -9,10 +9,10 @@ from models.provider import Provider @message_was_created.connect def handle(sender, **kwargs): message = sender - application_generate_entity: ApplicationGenerateEntity = kwargs.get('application_generate_entity') + application_generate_entity: EasyUIBasedAppGenerateEntity = kwargs.get('application_generate_entity') db.session.query(Provider).filter( - Provider.tenant_id == application_generate_entity.tenant_id, - Provider.provider_name == application_generate_entity.app_orchestration_config_entity.model_config.provider + Provider.tenant_id == application_generate_entity.app_config.tenant_id, + Provider.provider_name == application_generate_entity.model_config.provider ).update({'last_used': datetime.utcnow()}) db.session.commit() diff --git a/api/models/model.py b/api/models/model.py index 9efc9482f8..a8ae474c02 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -105,6 +105,18 @@ class App(db.Model): tenant = db.session.query(Tenant).filter(Tenant.id == self.tenant_id).first() return tenant + @property + def is_agent(self) -> bool: + app_model_config = self.app_model_config + if not app_model_config: + return False + if not app_model_config.agent_mode: + return False + if self.app_model_config.agent_mode_dict.get('enabled', False) \ + and self.app_model_config.agent_mode_dict.get('strategy', '') in ['function_call', 'react']: + return True + return False + @property def deleted_tools(self) -> list: # get agent mode tools diff --git a/api/models/workflow.py b/api/models/workflow.py index ff4e944e29..f9c906b85c 100644 --- a/api/models/workflow.py +++ b/api/models/workflow.py @@ -129,7 +129,7 @@ class Workflow(db.Model): def features_dict(self): return self.features if not self.features else json.loads(self.features) - def user_input_form(self): + def user_input_form(self) -> list: # get start node from graph if not self.graph: return [] diff --git a/api/services/app_model_config_service.py b/api/services/app_model_config_service.py index f2caeb14ff..c84f6fbf45 100644 --- a/api/services/app_model_config_service.py +++ b/api/services/app_model_config_service.py @@ -1,6 +1,6 @@ -from core.app.agent_chat.config_validator import AgentChatAppConfigValidator -from core.app.chat.config_validator import ChatAppConfigValidator -from core.app.completion.config_validator import CompletionAppConfigValidator +from core.app.apps.agent_chat.app_config_manager import AgentChatAppConfigManager +from core.app.apps.chat.app_config_manager import ChatAppConfigManager +from core.app.apps.completion.app_config_manager import CompletionAppConfigManager from models.model import AppMode @@ -9,10 +9,10 @@ class AppModelConfigService: @classmethod 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) + return ChatAppConfigManager.config_validate(tenant_id, config) elif app_mode == AppMode.AGENT_CHAT: - return AgentChatAppConfigValidator.config_validate(tenant_id, config) + return AgentChatAppConfigManager.config_validate(tenant_id, config) elif app_mode == AppMode.COMPLETION: - return CompletionAppConfigValidator.config_validate(tenant_id, config) + return CompletionAppConfigManager.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 8a9639e521..453194feb1 100644 --- a/api/services/completion_service.py +++ b/api/services/completion_service.py @@ -4,9 +4,9 @@ from typing import Any, Union from sqlalchemy import and_ -from core.app.app_manager import AppManager -from core.app.validators.model_validator import ModelValidator -from core.entities.application_entities import InvokeFrom +from core.app.app_config.features.file_upload.manager import FileUploadConfigManager +from core.app.app_manager import EasyUIBasedAppManager +from core.app.entities.app_invoke_entities import InvokeFrom from core.file.message_file_parser import MessageFileParser from extensions.ext_database import db from models.model import Account, App, AppMode, AppModelConfig, Conversation, EndUser, Message @@ -30,7 +30,7 @@ class CompletionService: auto_generate_name = args['auto_generate_name'] \ if 'auto_generate_name' in args else True - if app_model.mode != 'completion': + if app_model.mode != AppMode.COMPLETION.value: if not query: raise ValueError('query is required') @@ -43,6 +43,7 @@ class CompletionService: conversation_id = args['conversation_id'] if 'conversation_id' in args else None conversation = None + app_model_config_dict = None if conversation_id: conversation_filter = [ Conversation.id == args['conversation_id'], @@ -63,42 +64,13 @@ class CompletionService: if conversation.status != 'normal': raise ConversationCompletedError() - if not conversation.override_model_configs: - app_model_config = db.session.query(AppModelConfig).filter( - AppModelConfig.id == conversation.app_model_config_id, - AppModelConfig.app_id == app_model.id - ).first() + app_model_config = db.session.query(AppModelConfig).filter( + AppModelConfig.id == conversation.app_model_config_id, + AppModelConfig.app_id == app_model.id + ).first() - if not app_model_config: - raise AppModelConfigBrokenError() - else: - conversation_override_model_configs = json.loads(conversation.override_model_configs) - - app_model_config = AppModelConfig( - id=conversation.app_model_config_id, - app_id=app_model.id, - ) - - app_model_config = app_model_config.from_model_config_dict(conversation_override_model_configs) - - if is_model_config_override: - # build new app model config - if 'model' not in args['model_config']: - raise ValueError('model_config.model is required') - - if 'completion_params' not in args['model_config']['model']: - raise ValueError('model_config.model.completion_params is required') - - completion_params = ModelValidator.validate_model_completion_params( - cp=args['model_config']['model']['completion_params'] - ) - - app_model_config_model = app_model_config.model_dict - app_model_config_model['completion_params'] = completion_params - app_model_config.retriever_resource = json.dumps({'enabled': True}) - - app_model_config = app_model_config.copy() - app_model_config.model = json.dumps(app_model_config_model) + if not app_model_config: + raise AppModelConfigBrokenError() else: if app_model.app_model_config_id is None: raise AppModelConfigBrokenError() @@ -113,37 +85,29 @@ class CompletionService: raise Exception("Only account can override model config") # validate config - model_config = AppModelConfigService.validate_configuration( + app_model_config_dict = AppModelConfigService.validate_configuration( tenant_id=app_model.tenant_id, config=args['model_config'], app_mode=AppMode.value_of(app_model.mode) ) - app_model_config = AppModelConfig( - id=app_model_config.id, - app_id=app_model.id, - ) - - app_model_config = app_model_config.from_model_config_dict(model_config) - - # clean input by app_model_config form rules - inputs = cls.get_cleaned_inputs(inputs, app_model_config) - # parse files message_file_parser = MessageFileParser(tenant_id=app_model.tenant_id, app_id=app_model.id) - file_objs = message_file_parser.validate_and_transform_files_arg( - files, - app_model_config, - user - ) + file_upload_entity = FileUploadConfigManager.convert(app_model_config_dict or app_model_config.to_dict()) + if file_upload_entity: + file_objs = message_file_parser.validate_and_transform_files_arg( + files, + file_upload_entity, + user + ) + else: + file_objs = [] - application_manager = AppManager() + application_manager = EasyUIBasedAppManager() return application_manager.generate( - tenant_id=app_model.tenant_id, - app_id=app_model.id, - app_model_config_id=app_model_config.id, - app_model_config_dict=app_model_config.to_dict(), - app_model_config_override=is_model_config_override, + app_model=app_model, + app_model_config=app_model_config, + app_model_config_dict=app_model_config_dict, user=user, invoke_from=invoke_from, inputs=inputs, @@ -189,17 +153,19 @@ class CompletionService: # parse files message_file_parser = MessageFileParser(tenant_id=app_model.tenant_id, app_id=app_model.id) - file_objs = message_file_parser.transform_message_files( - message.files, app_model_config - ) + file_upload_entity = FileUploadConfigManager.convert(current_app_model_config.to_dict()) + if file_upload_entity: + file_objs = message_file_parser.transform_message_files( + message.files, file_upload_entity + ) + else: + file_objs = [] - application_manager = AppManager() + application_manager = EasyUIBasedAppManager() return application_manager.generate( - tenant_id=app_model.tenant_id, - app_id=app_model.id, - app_model_config_id=app_model_config.id, + app_model=app_model, + app_model_config=current_app_model_config, app_model_config_dict=app_model_config.to_dict(), - app_model_config_override=True, user=user, invoke_from=invoke_from, inputs=message.inputs, @@ -212,46 +178,3 @@ class CompletionService: } ) - @classmethod - def get_cleaned_inputs(cls, user_inputs: dict, app_model_config: AppModelConfig): - if user_inputs is None: - user_inputs = {} - - filtered_inputs = {} - - # Filter input variables from form configuration, handle required fields, default values, and option values - input_form_config = app_model_config.user_input_form_list - for config in input_form_config: - input_config = list(config.values())[0] - variable = input_config["variable"] - - input_type = list(config.keys())[0] - - if variable not in user_inputs or not user_inputs[variable]: - if input_type == "external_data_tool": - continue - if "required" in input_config and input_config["required"]: - raise ValueError(f"{variable} is required in input form") - else: - filtered_inputs[variable] = input_config["default"] if "default" in input_config else "" - continue - - value = user_inputs[variable] - - if value: - if not isinstance(value, str): - raise ValueError(f"{variable} in input form must be a string") - - if input_type == "select": - options = input_config["options"] if "options" in input_config else [] - if value not in options: - raise ValueError(f"{variable} in input form must be one of the following: {options}") - else: - if 'max_length' in input_config: - max_length = input_config['max_length'] - if len(value) > max_length: - raise ValueError(f'{variable} in input form must be less than {max_length} characters') - - filtered_inputs[variable] = value.replace('\x00', '') if value else None - - return filtered_inputs diff --git a/api/services/workflow/workflow_converter.py b/api/services/workflow/workflow_converter.py index 6c0182dd9e..d62f198014 100644 --- a/api/services/workflow/workflow_converter.py +++ b/api/services/workflow/workflow_converter.py @@ -1,16 +1,9 @@ import json from typing import Optional -from core.app.app_manager import AppManager -from core.entities.application_entities import ( - DatasetEntity, - DatasetRetrieveConfigEntity, - ExternalDataVariableEntity, - FileUploadEntity, - ModelConfigEntity, - PromptTemplateEntity, - VariableEntity, -) +from core.app.app_config.entities import VariableEntity, ExternalDataVariableEntity, DatasetEntity, \ + DatasetRetrieveConfigEntity, ModelConfigEntity, PromptTemplateEntity, FileUploadEntity +from core.app.app_manager import EasyUIBasedAppManager from core.helper import encrypter from core.model_runtime.entities.llm_entities import LLMMode from core.model_runtime.utils.encoders import jsonable_encoder @@ -36,7 +29,7 @@ class WorkflowConverter: - basic mode of chatbot app - - advanced mode of assistant app + - expert mode of chatbot app - completion app @@ -86,14 +79,11 @@ class WorkflowConverter: # get new app mode new_app_mode = self._get_new_app_mode(app_model) - app_model_config_dict = app_model_config.to_dict() - # convert app model config - application_manager = AppManager() - app_orchestration_config_entity = application_manager.convert_from_app_model_config_dict( - tenant_id=app_model.tenant_id, - app_model_config_dict=app_model_config_dict, - skip_check=True + application_manager = EasyUIBasedAppManager() + app_config = application_manager.convert_to_app_config( + app_model=app_model, + app_model_config=app_model_config ) # init workflow graph @@ -113,27 +103,27 @@ class WorkflowConverter: # convert to start node start_node = self._convert_to_start_node( - variables=app_orchestration_config_entity.variables + variables=app_config.variables ) graph['nodes'].append(start_node) # convert to http request node - if app_orchestration_config_entity.external_data_variables: + if app_config.external_data_variables: http_request_nodes = self._convert_to_http_request_node( app_model=app_model, - variables=app_orchestration_config_entity.variables, - external_data_variables=app_orchestration_config_entity.external_data_variables + variables=app_config.variables, + external_data_variables=app_config.external_data_variables ) for http_request_node in http_request_nodes: graph = self._append_node(graph, http_request_node) # convert to knowledge retrieval node - if app_orchestration_config_entity.dataset: + if app_config.dataset: knowledge_retrieval_node = self._convert_to_knowledge_retrieval_node( new_app_mode=new_app_mode, - dataset_config=app_orchestration_config_entity.dataset + dataset_config=app_config.dataset ) if knowledge_retrieval_node: @@ -143,9 +133,9 @@ class WorkflowConverter: llm_node = self._convert_to_llm_node( new_app_mode=new_app_mode, graph=graph, - model_config=app_orchestration_config_entity.model_config, - prompt_template=app_orchestration_config_entity.prompt_template, - file_upload=app_orchestration_config_entity.file_upload + model_config=app_config.model, + prompt_template=app_config.prompt_template, + file_upload=app_config.additional_features.file_upload ) graph = self._append_node(graph, llm_node) @@ -155,6 +145,8 @@ class WorkflowConverter: graph = self._append_node(graph, end_node) + app_model_config_dict = app_config.app_model_config_dict + # features if new_app_mode == AppMode.ADVANCED_CHAT: features = { diff --git a/api/services/workflow_service.py b/api/services/workflow_service.py index 102c861733..c9efd056ff 100644 --- a/api/services/workflow_service.py +++ b/api/services/workflow_service.py @@ -2,8 +2,8 @@ import json from datetime import datetime from typing import Optional -from core.app.advanced_chat.config_validator import AdvancedChatAppConfigValidator -from core.app.workflow.config_validator import WorkflowAppConfigValidator +from core.app.apps.advanced_chat.app_config_manager import AdvancedChatAppConfigManager +from core.app.apps.workflow.app_config_manager import WorkflowAppConfigManager from extensions.ext_database import db from models.account import Account from models.model import App, AppMode @@ -162,13 +162,13 @@ class WorkflowService: def validate_features_structure(self, app_model: App, features: dict) -> dict: if app_model.mode == AppMode.ADVANCED_CHAT.value: - return AdvancedChatAppConfigValidator.config_validate( + return AdvancedChatAppConfigManager.config_validate( tenant_id=app_model.tenant_id, config=features, only_structure_validate=True ) elif app_model.mode == AppMode.WORKFLOW.value: - return WorkflowAppConfigValidator.config_validate( + return WorkflowAppConfigManager.config_validate( tenant_id=app_model.tenant_id, config=features, only_structure_validate=True diff --git a/api/tests/unit_tests/core/prompt/test_advanced_prompt_transform.py b/api/tests/unit_tests/core/prompt/test_advanced_prompt_transform.py index 69acb23681..4357c6405c 100644 --- a/api/tests/unit_tests/core/prompt/test_advanced_prompt_transform.py +++ b/api/tests/unit_tests/core/prompt/test_advanced_prompt_transform.py @@ -2,8 +2,8 @@ from unittest.mock import MagicMock import pytest -from core.entities.application_entities import PromptTemplateEntity, AdvancedCompletionPromptTemplateEntity, \ - ModelConfigEntity, AdvancedChatPromptTemplateEntity, AdvancedChatMessageEntity +from core.app.app_config.entities import PromptTemplateEntity, AdvancedCompletionPromptTemplateEntity, \ + ModelConfigEntity, AdvancedChatPromptTemplateEntity, AdvancedChatMessageEntity, FileUploadEntity from core.file.file_obj import FileObj, FileType, FileTransferMethod from core.memory.token_buffer_memory import TokenBufferMemory from core.model_runtime.entities.message_entities import UserPromptMessage, AssistantPromptMessage, PromptMessageRole @@ -137,11 +137,11 @@ def test__get_chat_model_prompt_messages_with_files_no_memory(get_chat_model_arg type=FileType.IMAGE, transfer_method=FileTransferMethod.REMOTE_URL, url="https://example.com/image1.jpg", - file_config={ - "image": { + file_upload_entity=FileUploadEntity( + image_config={ "detail": "high", } - } + ) ) ] diff --git a/api/tests/unit_tests/core/prompt/test_prompt_transform.py b/api/tests/unit_tests/core/prompt/test_prompt_transform.py index 8a260b0507..9796fc5558 100644 --- a/api/tests/unit_tests/core/prompt/test_prompt_transform.py +++ b/api/tests/unit_tests/core/prompt/test_prompt_transform.py @@ -1,6 +1,6 @@ from unittest.mock import MagicMock -from core.entities.application_entities import ModelConfigEntity +from core.app.app_config.entities import ModelConfigEntity from core.entities.provider_configuration import ProviderModelBundle from core.model_runtime.entities.message_entities import UserPromptMessage from core.model_runtime.entities.model_entities import ModelPropertyKey, AIModelEntity, ParameterRule diff --git a/api/tests/unit_tests/core/prompt/test_simple_prompt_transform.py b/api/tests/unit_tests/core/prompt/test_simple_prompt_transform.py index a95a6dc52f..70f6070c6b 100644 --- a/api/tests/unit_tests/core/prompt/test_simple_prompt_transform.py +++ b/api/tests/unit_tests/core/prompt/test_simple_prompt_transform.py @@ -1,6 +1,6 @@ from unittest.mock import MagicMock -from core.entities.application_entities import ModelConfigEntity +from core.app.entities.app_invoke_entities import EasyUIBasedModelConfigEntity from core.memory.token_buffer_memory import TokenBufferMemory from core.model_runtime.entities.message_entities import UserPromptMessage, AssistantPromptMessage from core.prompt.simple_prompt_transform import SimplePromptTransform @@ -139,7 +139,7 @@ def test_get_common_chat_app_prompt_template_with_p(): def test__get_chat_model_prompt_messages(): - model_config_mock = MagicMock(spec=ModelConfigEntity) + model_config_mock = MagicMock(spec=EasyUIBasedModelConfigEntity) model_config_mock.provider = 'openai' model_config_mock.model = 'gpt-4' @@ -191,7 +191,7 @@ def test__get_chat_model_prompt_messages(): def test__get_completion_model_prompt_messages(): - model_config_mock = MagicMock(spec=ModelConfigEntity) + model_config_mock = MagicMock(spec=EasyUIBasedModelConfigEntity) model_config_mock.provider = 'openai' model_config_mock.model = 'gpt-3.5-turbo-instruct' diff --git a/api/tests/unit_tests/services/workflow/test_workflow_converter.py b/api/tests/unit_tests/services/workflow/test_workflow_converter.py index d4edc73410..0ca8ae135c 100644 --- a/api/tests/unit_tests/services/workflow/test_workflow_converter.py +++ b/api/tests/unit_tests/services/workflow/test_workflow_converter.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock import pytest -from core.entities.application_entities import VariableEntity, ExternalDataVariableEntity, DatasetEntity, \ +from core.app.app_config.entities import VariableEntity, ExternalDataVariableEntity, DatasetEntity, \ DatasetRetrieveConfigEntity, ModelConfigEntity, PromptTemplateEntity, AdvancedChatPromptTemplateEntity, \ AdvancedChatMessageEntity, AdvancedCompletionPromptTemplateEntity from core.helper import encrypter