From 6e3cd62e311b24928a0c48fb07bd7b71557d4628 Mon Sep 17 00:00:00 2001 From: takatost Date: Tue, 27 Feb 2024 13:23:01 +0800 Subject: [PATCH] refactor app mode add app import and export --- api/constants/languages.py | 436 ------------------ api/constants/model_template.py | 99 ++-- api/controllers/console/app/app.py | 239 ++++++---- api/controllers/console/app/workflow.py | 11 +- api/controllers/console/app/wraps.py | 18 +- .../console/explore/installed_app.py | 3 +- .../console/explore/recommended_app.py | 64 +-- api/core/provider_manager.py | 2 +- api/fields/app_fields.py | 12 - api/fields/installed_app_fields.py | 3 +- .../versions/b289e2408ee2_add_workflow.py | 2 - ...998d4d_set_model_config_column_nullable.py | 70 +++ api/models/model.py | 53 +-- api/services/workflow/workflow_converter.py | 4 +- api/services/workflow_service.py | 43 +- 15 files changed, 371 insertions(+), 688 deletions(-) create mode 100644 api/migrations/versions/cc04d0998d4d_set_model_config_column_nullable.py diff --git a/api/constants/languages.py b/api/constants/languages.py index 284f3d8758..b89ac98db9 100644 --- a/api/constants/languages.py +++ b/api/constants/languages.py @@ -26,439 +26,3 @@ def supported_language(lang): error = ('{lang} is not a valid language.' .format(lang=lang)) raise ValueError(error) - - -user_input_form_template = { - "en-US": [ - { - "paragraph": { - "label": "Query", - "variable": "default_input", - "required": False, - "default": "" - } - } - ], - "zh-Hans": [ - { - "paragraph": { - "label": "查询内容", - "variable": "default_input", - "required": False, - "default": "" - } - } - ], - "pt-BR": [ - { - "paragraph": { - "label": "Consulta", - "variable": "default_input", - "required": False, - "default": "" - } - } - ], - "es-ES": [ - { - "paragraph": { - "label": "Consulta", - "variable": "default_input", - "required": False, - "default": "" - } - } - ], - "ua-UK": [ - { - "paragraph": { - "label": "Запит", - "variable": "default_input", - "required": False, - "default": "" - } - } - ], -} - -demo_model_templates = { - 'en-US': [ - { - 'name': 'Translation Assistant', - 'icon': '', - 'icon_background': '', - 'description': 'A multilingual translator that provides translation capabilities in multiple languages, translating user input into the language they need.', - 'mode': 'completion', - 'model_config': AppModelConfig( - provider='openai', - model_id='gpt-3.5-turbo-instruct', - configs={ - 'prompt_template': "Please translate the following text into {{target_language}}:\n", - 'prompt_variables': [ - { - "key": "target_language", - "name": "Target Language", - "description": "The language you want to translate into.", - "type": "select", - "default": "Chinese", - 'options': [ - 'Chinese', - 'English', - 'Japanese', - 'French', - 'Russian', - 'German', - 'Spanish', - 'Korean', - 'Italian', - ] - } - ], - 'completion_params': { - 'max_token': 1000, - 'temperature': 0, - 'top_p': 0, - 'presence_penalty': 0.1, - 'frequency_penalty': 0.1, - } - }, - opening_statement='', - suggested_questions=None, - pre_prompt="Please translate the following text into {{target_language}}:\n{{query}}\ntranslate:", - model=json.dumps({ - "provider": "openai", - "name": "gpt-3.5-turbo-instruct", - "mode": "completion", - "completion_params": { - "max_tokens": 1000, - "temperature": 0, - "top_p": 0, - "presence_penalty": 0.1, - "frequency_penalty": 0.1 - } - }), - user_input_form=json.dumps([ - { - "select": { - "label": "Target Language", - "variable": "target_language", - "description": "The language you want to translate into.", - "default": "Chinese", - "required": True, - 'options': [ - 'Chinese', - 'English', - 'Japanese', - 'French', - 'Russian', - 'German', - 'Spanish', - 'Korean', - 'Italian', - ] - } - }, { - "paragraph": { - "label": "Query", - "variable": "query", - "required": True, - "default": "" - } - } - ]) - ) - }, - { - 'name': 'AI Front-end Interviewer', - 'icon': '', - 'icon_background': '', - 'description': 'A simulated front-end interviewer that tests the skill level of front-end development through questioning.', - 'mode': 'chat', - 'model_config': AppModelConfig( - provider='openai', - model_id='gpt-3.5-turbo', - configs={ - 'introduction': 'Hi, welcome to our interview. I am the interviewer for this technology company, and I will test your web front-end development skills. Next, I will ask you some technical questions. Please answer them as thoroughly as possible. ', - 'prompt_template': "You will play the role of an interviewer for a technology company, examining the user's web front-end development skills and posing 5-10 sharp technical questions.\n\nPlease note:\n- Only ask one question at a time.\n- After the user answers a question, ask the next question directly, without trying to correct any mistakes made by the candidate.\n- If you think the user has not answered correctly for several consecutive questions, ask fewer questions.\n- After asking the last question, you can ask this question: Why did you leave your last job? After the user answers this question, please express your understanding and support.\n", - 'prompt_variables': [], - 'completion_params': { - 'max_token': 300, - 'temperature': 0.8, - 'top_p': 0.9, - 'presence_penalty': 0.1, - 'frequency_penalty': 0.1, - } - }, - opening_statement='Hi, welcome to our interview. I am the interviewer for this technology company, and I will test your web front-end development skills. Next, I will ask you some technical questions. Please answer them as thoroughly as possible. ', - suggested_questions=None, - pre_prompt="You will play the role of an interviewer for a technology company, examining the user's web front-end development skills and posing 5-10 sharp technical questions.\n\nPlease note:\n- Only ask one question at a time.\n- After the user answers a question, ask the next question directly, without trying to correct any mistakes made by the candidate.\n- If you think the user has not answered correctly for several consecutive questions, ask fewer questions.\n- After asking the last question, you can ask this question: Why did you leave your last job? After the user answers this question, please express your understanding and support.\n", - model=json.dumps({ - "provider": "openai", - "name": "gpt-3.5-turbo", - "mode": "chat", - "completion_params": { - "max_tokens": 300, - "temperature": 0.8, - "top_p": 0.9, - "presence_penalty": 0.1, - "frequency_penalty": 0.1 - } - }), - user_input_form=None - ) - } - ], - - 'zh-Hans': [ - { - 'name': '翻译助手', - 'icon': '', - 'icon_background': '', - 'description': '一个多语言翻译器,提供多种语言翻译能力,将用户输入的文本翻译成他们需要的语言。', - 'mode': 'completion', - 'model_config': AppModelConfig( - provider='openai', - model_id='gpt-3.5-turbo-instruct', - configs={ - 'prompt_template': "请将以下文本翻译为{{target_language}}:\n", - 'prompt_variables': [ - { - "key": "target_language", - "name": "目标语言", - "description": "翻译的目标语言", - "type": "select", - "default": "中文", - "options": [ - "中文", - "英文", - "日语", - "法语", - "俄语", - "德语", - "西班牙语", - "韩语", - "意大利语", - ] - } - ], - 'completion_params': { - 'max_token': 1000, - 'temperature': 0, - 'top_p': 0, - 'presence_penalty': 0.1, - 'frequency_penalty': 0.1, - } - }, - opening_statement='', - suggested_questions=None, - pre_prompt="请将以下文本翻译为{{target_language}}:\n{{query}}\n翻译:", - model=json.dumps({ - "provider": "openai", - "name": "gpt-3.5-turbo-instruct", - "mode": "completion", - "completion_params": { - "max_tokens": 1000, - "temperature": 0, - "top_p": 0, - "presence_penalty": 0.1, - "frequency_penalty": 0.1 - } - }), - user_input_form=json.dumps([ - { - "select": { - "label": "目标语言", - "variable": "target_language", - "description": "翻译的目标语言", - "default": "中文", - "required": True, - 'options': [ - "中文", - "英文", - "日语", - "法语", - "俄语", - "德语", - "西班牙语", - "韩语", - "意大利语", - ] - } - }, { - "paragraph": { - "label": "文本内容", - "variable": "query", - "required": True, - "default": "" - } - } - ]) - ) - }, - { - 'name': 'AI 前端面试官', - 'icon': '', - 'icon_background': '', - 'description': '一个模拟的前端面试官,通过提问的方式对前端开发的技能水平进行检验。', - 'mode': 'chat', - 'model_config': AppModelConfig( - provider='openai', - model_id='gpt-3.5-turbo', - configs={ - 'introduction': '你好,欢迎来参加我们的面试,我是这家科技公司的面试官,我将考察你的 Web 前端开发技能。接下来我会向您提出一些技术问题,请您尽可能详尽地回答。', - 'prompt_template': "你将扮演一个科技公司的面试官,考察用户作为候选人的 Web 前端开发水平,提出 5-10 个犀利的技术问题。\n\n请注意:\n- 每次只问一个问题\n- 用户回答问题后请直接问下一个问题,而不要试图纠正候选人的错误;\n- 如果你认为用户连续几次回答的都不对,就少问一点;\n- 问完最后一个问题后,你可以问这样一个问题:上一份工作为什么离职?用户回答该问题后,请表示理解与支持。\n", - 'prompt_variables': [], - 'completion_params': { - 'max_token': 300, - 'temperature': 0.8, - 'top_p': 0.9, - 'presence_penalty': 0.1, - 'frequency_penalty': 0.1, - } - }, - opening_statement='你好,欢迎来参加我们的面试,我是这家科技公司的面试官,我将考察你的 Web 前端开发技能。接下来我会向您提出一些技术问题,请您尽可能详尽地回答。', - suggested_questions=None, - pre_prompt="你将扮演一个科技公司的面试官,考察用户作为候选人的 Web 前端开发水平,提出 5-10 个犀利的技术问题。\n\n请注意:\n- 每次只问一个问题\n- 用户回答问题后请直接问下一个问题,而不要试图纠正候选人的错误;\n- 如果你认为用户连续几次回答的都不对,就少问一点;\n- 问完最后一个问题后,你可以问这样一个问题:上一份工作为什么离职?用户回答该问题后,请表示理解与支持。\n", - model=json.dumps({ - "provider": "openai", - "name": "gpt-3.5-turbo", - "mode": "chat", - "completion_params": { - "max_tokens": 300, - "temperature": 0.8, - "top_p": 0.9, - "presence_penalty": 0.1, - "frequency_penalty": 0.1 - } - }), - user_input_form=None - ) - } - ], - 'uk-UA': [{ - "name": "Помічник перекладу", - "icon": "", - "icon_background": "", - "description": "Багатомовний перекладач, який надає можливості перекладу різними мовами, перекладаючи введені користувачем дані на потрібну мову.", - "mode": "completion", - "model_config": AppModelConfig( - provider="openai", - model_id="gpt-3.5-turbo-instruct", - configs={ - "prompt_template": "Будь ласка, перекладіть наступний текст на {{target_language}}:\n", - "prompt_variables": [ - { - "key": "target_language", - "name": "Цільова мова", - "description": "Мова, на яку ви хочете перекласти.", - "type": "select", - "default": "Ukrainian", - "options": [ - "Chinese", - "English", - "Japanese", - "French", - "Russian", - "German", - "Spanish", - "Korean", - "Italian", - ], - }, - ], - "completion_params": { - "max_token": 1000, - "temperature": 0, - "top_p": 0, - "presence_penalty": 0.1, - "frequency_penalty": 0.1, - }, - }, - opening_statement="", - suggested_questions=None, - pre_prompt="Будь ласка, перекладіть наступний текст на {{target_language}}:\n{{query}}\ntranslate:", - model=json.dumps({ - "provider": "openai", - "name": "gpt-3.5-turbo-instruct", - "mode": "completion", - "completion_params": { - "max_tokens": 1000, - "temperature": 0, - "top_p": 0, - "presence_penalty": 0.1, - "frequency_penalty": 0.1, - }, - }), - user_input_form=json.dumps([ - { - "select": { - "label": "Цільова мова", - "variable": "target_language", - "description": "Мова, на яку ви хочете перекласти.", - "default": "Chinese", - "required": True, - 'options': [ - 'Chinese', - 'English', - 'Japanese', - 'French', - 'Russian', - 'German', - 'Spanish', - 'Korean', - 'Italian', - ] - } - }, { - "paragraph": { - "label": "Запит", - "variable": "query", - "required": True, - "default": "" - } - } - ]) - ) - }, - { - "name": "AI інтерв’юер фронтенду", - "icon": "", - "icon_background": "", - "description": "Симульований інтерв’юер фронтенду, який перевіряє рівень кваліфікації у розробці фронтенду через опитування.", - "mode": "chat", - "model_config": AppModelConfig( - provider="openai", - model_id="gpt-3.5-turbo", - configs={ - "introduction": "Привіт, ласкаво просимо на наше співбесіду. Я інтерв'юер цієї технологічної компанії, і я перевірю ваші навички веб-розробки фронтенду. Далі я поставлю вам декілька технічних запитань. Будь ласка, відповідайте якомога ретельніше. ", - "prompt_template": "Ви будете грати роль інтерв'юера технологічної компанії, перевіряючи навички розробки фронтенду користувача та ставлячи 5-10 чітких технічних питань.\n\nЗверніть увагу:\n- Ставте лише одне запитання за раз.\n- Після того, як користувач відповість на запитання, ставте наступне запитання безпосередньо, не намагаючись виправити будь-які помилки, допущені кандидатом.\n- Якщо ви вважаєте, що користувач не відповів правильно на кілька питань поспіль, задайте менше запитань.\n- Після того, як ви задали останнє запитання, ви можете поставити таке запитання: Чому ви залишили свою попередню роботу? Після того, як користувач відповість на це питання, висловіть своє розуміння та підтримку.\n", - "prompt_variables": [], - "completion_params": { - "max_token": 300, - "temperature": 0.8, - "top_p": 0.9, - "presence_penalty": 0.1, - "frequency_penalty": 0.1, - }, - }, - opening_statement="Привіт, ласкаво просимо на наше співбесіду. Я інтерв'юер цієї технологічної компанії, і я перевірю ваші навички веб-розробки фронтенду. Далі я поставлю вам декілька технічних запитань. Будь ласка, відповідайте якомога ретельніше. ", - suggested_questions=None, - pre_prompt="Ви будете грати роль інтерв'юера технологічної компанії, перевіряючи навички розробки фронтенду користувача та ставлячи 5-10 чітких технічних питань.\n\nЗверніть увагу:\n- Ставте лише одне запитання за раз.\n- Після того, як користувач відповість на запитання, ставте наступне запитання безпосередньо, не намагаючись виправити будь-які помилки, допущені кандидатом.\n- Якщо ви вважаєте, що користувач не відповів правильно на кілька питань поспіль, задайте менше запитань.\n- Після того, як ви задали останнє запитання, ви можете поставити таке запитання: Чому ви залишили свою попередню роботу? Після того, як користувач відповість на це питання, висловіть своє розуміння та підтримку.\n", - model=json.dumps({ - "provider": "openai", - "name": "gpt-3.5-turbo", - "mode": "chat", - "completion_params": { - "max_tokens": 300, - "temperature": 0.8, - "top_p": 0.9, - "presence_penalty": 0.1, - "frequency_penalty": 0.1, - }, - }), - user_input_form=None - ), - } - ], - -} diff --git a/api/constants/model_template.py b/api/constants/model_template.py index c22306ac87..ca0b754989 100644 --- a/api/constants/model_template.py +++ b/api/constants/model_template.py @@ -1,50 +1,25 @@ -import json +from models.model import AppMode -model_templates = { +default_app_templates = { # workflow default mode - 'workflow_default': { + AppMode.WORKFLOW: { 'app': { - 'mode': 'workflow', + 'mode': AppMode.WORKFLOW.value, 'enable_site': True, - 'enable_api': True, - 'is_demo': False, - 'api_rpm': 0, - 'api_rph': 0, - 'status': 'normal' + 'enable_api': True }, - 'model_config': { - 'provider': '', - 'model_id': '', - 'configs': {} - } + 'model_config': {} }, # chat default mode - 'chat_default': { + AppMode.CHAT: { 'app': { - 'mode': 'chat', + 'mode': AppMode.CHAT.value, 'enable_site': True, - 'enable_api': True, - 'is_demo': False, - 'api_rpm': 0, - 'api_rph': 0, - 'status': 'normal' + 'enable_api': True }, 'model_config': { - 'provider': 'openai', - 'model_id': 'gpt-4', - 'configs': { - 'prompt_template': '', - 'prompt_variables': [], - 'completion_params': { - 'max_token': 512, - 'temperature': 1, - 'top_p': 1, - 'presence_penalty': 0, - 'frequency_penalty': 0, - } - }, - 'model': json.dumps({ + 'model': { "provider": "openai", "name": "gpt-4", "mode": "chat", @@ -55,36 +30,19 @@ model_templates = { "presence_penalty": 0, "frequency_penalty": 0 } - }) + } } }, - # agent default mode - 'agent_default': { + # advanced-chat default mode + AppMode.ADVANCED_CHAT: { 'app': { - 'mode': 'agent', + 'mode': AppMode.ADVANCED_CHAT.value, 'enable_site': True, - 'enable_api': True, - 'is_demo': False, - 'api_rpm': 0, - 'api_rph': 0, - 'status': 'normal' + 'enable_api': True }, 'model_config': { - 'provider': 'openai', - 'model_id': 'gpt-4', - 'configs': { - 'prompt_template': '', - 'prompt_variables': [], - 'completion_params': { - 'max_token': 512, - 'temperature': 1, - 'top_p': 1, - 'presence_penalty': 0, - 'frequency_penalty': 0, - } - }, - 'model': json.dumps({ + 'model': { "provider": "openai", "name": "gpt-4", "mode": "chat", @@ -95,7 +53,30 @@ model_templates = { "presence_penalty": 0, "frequency_penalty": 0 } - }) + } + } + }, + + # agent-chat default mode + AppMode.AGENT_CHAT: { + 'app': { + 'mode': AppMode.AGENT_CHAT.value, + 'enable_site': True, + 'enable_api': True + }, + 'model_config': { + 'model': { + "provider": "openai", + "name": "gpt-4", + "mode": "chat", + "completion_params": { + "max_tokens": 512, + "temperature": 1, + "top_p": 1, + "presence_penalty": 0, + "frequency_penalty": 0 + } + } } }, } diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index a1ab3e6ba2..52e97dd973 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -1,13 +1,15 @@ import json import logging from datetime import datetime +from typing import cast +import yaml from flask_login import current_user from flask_restful import Resource, abort, inputs, marshal_with, reqparse from werkzeug.exceptions import Forbidden -from constants.languages import demo_model_templates, languages -from constants.model_template import model_templates +from constants.languages import languages +from constants.model_template import default_app_templates from controllers.console import api from controllers.console.app.error import ProviderNotInitializeError from controllers.console.app.wraps import get_app_model @@ -15,7 +17,8 @@ from controllers.console.setup import setup_required from controllers.console.wraps import account_initialization_required, cloud_edition_billing_resource_check from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError from core.model_manager import ModelManager -from core.model_runtime.entities.model_entities import ModelType +from core.model_runtime.entities.model_entities import ModelType, ModelPropertyKey +from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel from core.provider_manager import ProviderManager from events.app_event import app_was_created, app_was_deleted from extensions.ext_database import db @@ -28,10 +31,15 @@ from fields.app_fields import ( from libs.login import login_required from models.model import App, AppModelConfig, Site, AppMode from services.app_model_config_service import AppModelConfigService +from services.workflow_service import WorkflowService from core.tools.utils.configuration import ToolParameterConfigurationManager from core.tools.tool_manager import ToolManager from core.entities.application_entities import AgentToolEntity + +ALLOW_CREATE_APP_MODES = ['chat', 'agent-chat', 'advanced-chat', 'workflow'] + + class AppListApi(Resource): @setup_required @@ -43,7 +51,7 @@ class AppListApi(Resource): parser = reqparse.RequestParser() parser.add_argument('page', type=inputs.int_range(1, 99999), required=False, default=1, location='args') parser.add_argument('limit', type=inputs.int_range(1, 100), required=False, default=20, location='args') - parser.add_argument('mode', type=str, choices=['chat', 'completion', 'all'], default='all', location='args', required=False) + parser.add_argument('mode', type=str, choices=['chat', 'workflow', 'agent', 'channel', 'all'], default='all', location='args', required=False) parser.add_argument('name', type=str, location='args', required=False) args = parser.parse_args() @@ -52,15 +60,20 @@ class AppListApi(Resource): App.is_universal == False ] - if args['mode'] == 'completion': - filters.append(App.mode == 'completion') + if args['mode'] == 'workflow': + filters.append(App.mode.in_([AppMode.WORKFLOW.value, AppMode.COMPLETION.value])) elif args['mode'] == 'chat': - filters.append(App.mode == 'chat') + filters.append(App.mode.in_([AppMode.CHAT.value, AppMode.ADVANCED_CHAT.value])) + elif args['mode'] == 'agent': + filters.append(App.mode == AppMode.AGENT_CHAT.value) + elif args['mode'] == 'channel': + filters.append(App.mode == AppMode.CHANNEL.value) else: pass if 'name' in args and args['name']: - filters.append(App.name.ilike(f'%{args["name"]}%')) + name = args['name'][:30] + filters.append(App.name.ilike(f'%{name}%')) app_models = db.paginate( db.select(App).where(*filters).order_by(App.created_at.desc()), @@ -80,10 +93,9 @@ class AppListApi(Resource): """Create app""" parser = reqparse.RequestParser() parser.add_argument('name', type=str, required=True, location='json') - parser.add_argument('mode', type=str, choices=['chat', 'agent', 'workflow'], location='json') + parser.add_argument('mode', type=str, choices=ALLOW_CREATE_APP_MODES, location='json') parser.add_argument('icon', type=str, location='json') parser.add_argument('icon_background', type=str, location='json') - parser.add_argument('model_config', type=dict, location='json') args = parser.parse_args() # The role of the current user in the ta table must be admin or owner @@ -141,15 +153,15 @@ class AppListApi(Resource): app_mode = AppMode.value_of(args['mode']) - model_config_template = model_templates[app_mode.value + '_default'] + app_template = default_app_templates[app_mode] - app = App(**model_config_template['app']) - app_model_config = AppModelConfig(**model_config_template['model_config']) - - if app_mode in [AppMode.CHAT, AppMode.AGENT]: + # get model config + default_model_config = app_template['model_config'] + if 'model' in default_model_config: # get model provider model_manager = ModelManager() + # get default model instance try: model_instance = model_manager.get_default_model_instance( tenant_id=current_user.current_tenant_id, @@ -159,10 +171,25 @@ class AppListApi(Resource): model_instance = None if model_instance: - model_dict = app_model_config.model_dict - model_dict['provider'] = model_instance.provider - model_dict['name'] = model_instance.model - app_model_config.model = json.dumps(model_dict) + if model_instance.model == default_model_config['model']['name']: + default_model_dict = default_model_config['model'] + else: + llm_model = cast(LargeLanguageModel, model_instance.model_type_instance) + model_schema = llm_model.get_model_schema(model_instance.model, model_instance.credentials) + + default_model_dict = { + 'provider': model_instance.provider, + 'name': model_instance.model, + 'mode': model_schema.model_properties.get(ModelPropertyKey.MODE), + 'completion_params': {} + } + else: + default_model_dict = default_model_config['model'] + + default_model_config['model'] = json.dumps(default_model_dict) + + app = App(**app_template['app']) + app_model_config = AppModelConfig(**default_model_config) app.name = args['name'] app.mode = args['mode'] @@ -195,24 +222,95 @@ class AppListApi(Resource): app_was_created.send(app) return app, 201 - -class AppTemplateApi(Resource): +class AppImportApi(Resource): @setup_required @login_required @account_initialization_required - @marshal_with(template_list_fields) - def get(self): - """Get app demo templates""" + @marshal_with(app_detail_fields) + @cloud_edition_billing_resource_check('apps') + def post(self): + """Import app""" + # The role of the current user in the ta table must be admin or owner + if not current_user.is_admin_or_owner: + raise Forbidden() + + parser = reqparse.RequestParser() + parser.add_argument('data', type=str, required=True, nullable=False, location='json') + parser.add_argument('name', type=str, location='json') + parser.add_argument('icon', type=str, location='json') + parser.add_argument('icon_background', type=str, location='json') + args = parser.parse_args() + + try: + import_data = yaml.safe_load(args['data']) + except yaml.YAMLError as e: + raise ValueError("Invalid YAML format in data argument.") + + app_data = import_data.get('app') + model_config_data = import_data.get('model_config') + workflow_graph = import_data.get('workflow_graph') + + if not app_data or not model_config_data: + raise ValueError("Missing app or model_config in data argument") + + app_mode = AppMode.value_of(app_data.get('mode')) + if app_mode in [AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]: + if not workflow_graph: + raise ValueError("Missing workflow_graph in data argument " + "when mode is advanced-chat or workflow") + + app = App( + enable_site=True, + enable_api=True, + is_demo=False, + api_rpm=0, + api_rph=0, + status='normal' + ) + + app.tenant_id = current_user.current_tenant_id + app.mode = app_data.get('mode') + app.name = args.get("name") if args.get("name") else app_data.get('name') + app.icon = args.get("icon") if args.get("icon") else app_data.get('icon') + app.icon_background = args.get("icon_background") if args.get("icon_background") \ + else app_data.get('icon_background') + + db.session.add(app) + db.session.commit() + + if workflow_graph: + workflow_service = WorkflowService() + draft_workflow = workflow_service.sync_draft_workflow(app, workflow_graph, current_user) + published_workflow = workflow_service.publish_draft_workflow(app, current_user, draft_workflow) + model_config_data['workflow_id'] = published_workflow.id + + app_model_config = AppModelConfig() + app_model_config = app_model_config.from_model_config_dict(model_config_data) + app_model_config.app_id = app.id + + db.session.add(app_model_config) + db.session.commit() + + app.app_model_config_id = app_model_config.id + account = current_user - interface_language = account.interface_language - templates = demo_model_templates.get(interface_language) - if not templates: - templates = demo_model_templates.get(languages[0]) + site = Site( + app_id=app.id, + title=app.name, + default_language=account.interface_language, + customize_token_strategy='not_allow', + code=Site.generate_code(16) + ) - return {'data': templates} + db.session.add(site) + db.session.commit() + + app_was_created.send(app) + + return app, 201 class AppApi(Resource): @@ -278,6 +376,38 @@ class AppApi(Resource): return {'result': 'success'}, 204 +class AppExportApi(Resource): + @setup_required + @login_required + @account_initialization_required + @get_app_model + def get(self, app_model): + """Export app""" + app_model_config = app_model.app_model_config + + export_data = { + "app": { + "name": app_model.name, + "mode": app_model.mode, + "icon": app_model.icon, + "icon_background": app_model.icon_background + }, + "model_config": app_model_config.to_dict(), + } + + if app_model_config.workflow_id: + export_data['workflow_graph'] = json.loads(app_model_config.workflow.graph) + else: + # get draft workflow + workflow_service = WorkflowService() + workflow = workflow_service.get_draft_workflow(app_model) + export_data['workflow_graph'] = json.loads(workflow.graph) + + return { + "data": yaml.dump(export_data) + } + + class AppNameApi(Resource): @setup_required @login_required @@ -355,57 +485,10 @@ class AppApiStatus(Resource): return app_model -class AppCopy(Resource): - @staticmethod - def create_app_copy(app): - copy_app = App( - name=app.name + ' copy', - icon=app.icon, - icon_background=app.icon_background, - tenant_id=app.tenant_id, - mode=app.mode, - app_model_config_id=app.app_model_config_id, - enable_site=app.enable_site, - enable_api=app.enable_api, - api_rpm=app.api_rpm, - api_rph=app.api_rph - ) - return copy_app - - @staticmethod - def create_app_model_config_copy(app_config, copy_app_id): - copy_app_model_config = app_config.copy() - copy_app_model_config.app_id = copy_app_id - - return copy_app_model_config - - @setup_required - @login_required - @account_initialization_required - @get_app_model - @marshal_with(app_detail_fields) - def post(self, app_model): - copy_app = self.create_app_copy(app_model) - db.session.add(copy_app) - - app_config = db.session.query(AppModelConfig). \ - filter(AppModelConfig.app_id == app_model.id). \ - one_or_none() - - if app_config: - copy_app_model_config = self.create_app_model_config_copy(app_config, copy_app.id) - db.session.add(copy_app_model_config) - db.session.commit() - copy_app.app_model_config_id = copy_app_model_config.id - db.session.commit() - - return copy_app, 201 - - api.add_resource(AppListApi, '/apps') -api.add_resource(AppTemplateApi, '/app-templates') +api.add_resource(AppImportApi, '/apps/import') api.add_resource(AppApi, '/apps/') -api.add_resource(AppCopy, '/apps//copy') +api.add_resource(AppExportApi, '/apps//export') api.add_resource(AppNameApi, '/apps//name') api.add_resource(AppIconApi, '/apps//icon') api.add_resource(AppSiteStatus, '/apps//site-enable') diff --git a/api/controllers/console/app/workflow.py b/api/controllers/console/app/workflow.py index dc1b7edcaf..6023d0ba45 100644 --- a/api/controllers/console/app/workflow.py +++ b/api/controllers/console/app/workflow.py @@ -7,7 +7,7 @@ from controllers.console.setup import setup_required from controllers.console.wraps import account_initialization_required from fields.workflow_fields import workflow_fields from libs.login import current_user, login_required -from models.model import App, AppMode, ChatbotAppEngine +from models.model import App, AppMode from services.workflow_service import WorkflowService @@ -15,7 +15,7 @@ class DraftWorkflowApi(Resource): @setup_required @login_required @account_initialization_required - @get_app_model(mode=[AppMode.CHAT, AppMode.WORKFLOW], app_engine=ChatbotAppEngine.WORKFLOW) + @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) @marshal_with(workflow_fields) def get(self, app_model: App): """ @@ -34,7 +34,7 @@ class DraftWorkflowApi(Resource): @setup_required @login_required @account_initialization_required - @get_app_model(mode=[AppMode.CHAT, AppMode.WORKFLOW], app_engine=ChatbotAppEngine.WORKFLOW) + @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) def post(self, app_model: App): """ Sync draft workflow @@ -55,7 +55,7 @@ class DefaultBlockConfigApi(Resource): @setup_required @login_required @account_initialization_required - @get_app_model(mode=[AppMode.CHAT, AppMode.WORKFLOW], app_engine=ChatbotAppEngine.WORKFLOW) + @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) def get(self, app_model: App): """ Get default block config @@ -72,7 +72,8 @@ class ConvertToWorkflowApi(Resource): @get_app_model(mode=[AppMode.CHAT, AppMode.COMPLETION]) def post(self, app_model: App): """ - Convert basic mode of chatbot app(expert mode) to workflow mode + Convert basic mode of chatbot app to workflow mode + Convert expert mode of chatbot app to workflow mode Convert Completion App to Workflow App """ # convert to workflow mode diff --git a/api/controllers/console/app/wraps.py b/api/controllers/console/app/wraps.py index 1c2c4cf5c7..d61ab6d6ae 100644 --- a/api/controllers/console/app/wraps.py +++ b/api/controllers/console/app/wraps.py @@ -5,12 +5,11 @@ from typing import Optional, Union from controllers.console.app.error import AppNotFoundError from extensions.ext_database import db from libs.login import current_user -from models.model import App, AppMode, ChatbotAppEngine +from models.model import App, AppMode def get_app_model(view: Optional[Callable] = None, *, - mode: Union[AppMode, list[AppMode]] = None, - app_engine: ChatbotAppEngine = None): + mode: Union[AppMode, list[AppMode]] = None): def decorator(view_func): @wraps(view_func) def decorated_view(*args, **kwargs): @@ -32,6 +31,9 @@ def get_app_model(view: Optional[Callable] = None, *, raise AppNotFoundError() app_mode = AppMode.value_of(app_model.mode) + if app_mode == AppMode.CHANNEL: + raise AppNotFoundError() + if mode is not None: if isinstance(mode, list): modes = mode @@ -42,16 +44,6 @@ def get_app_model(view: Optional[Callable] = None, *, mode_values = {m.value for m in modes} raise AppNotFoundError(f"App mode is not in the supported list: {mode_values}") - if app_engine is not None: - if app_mode not in [AppMode.CHAT, AppMode.WORKFLOW]: - raise AppNotFoundError(f"App mode is not supported for {app_engine.value} app engine.") - - if app_mode == AppMode.CHAT: - # fetch current app model config - app_model_config = app_model.app_model_config - if not app_model_config or app_model_config.chatbot_app_engine != app_engine.value: - raise AppNotFoundError(f"{app_engine.value} app engine is not supported.") - kwargs['app_model'] = app_model return view_func(*args, **kwargs) diff --git a/api/controllers/console/explore/installed_app.py b/api/controllers/console/explore/installed_app.py index 920d9141ae..7d6231270f 100644 --- a/api/controllers/console/explore/installed_app.py +++ b/api/controllers/console/explore/installed_app.py @@ -34,8 +34,7 @@ class InstalledAppsListApi(Resource): 'is_pinned': installed_app.is_pinned, 'last_used_at': installed_app.last_used_at, 'editable': current_user.role in ["owner", "admin"], - 'uninstallable': current_tenant_id == installed_app.app_owner_tenant_id, - 'is_agent': installed_app.is_agent + 'uninstallable': current_tenant_id == installed_app.app_owner_tenant_id } for installed_app in installed_apps ] diff --git a/api/controllers/console/explore/recommended_app.py b/api/controllers/console/explore/recommended_app.py index 6ba04d603a..3c28980f51 100644 --- a/api/controllers/console/explore/recommended_app.py +++ b/api/controllers/console/explore/recommended_app.py @@ -1,3 +1,6 @@ +import json + +import yaml from flask_login import current_user from flask_restful import Resource, fields, marshal_with, reqparse @@ -6,6 +9,7 @@ from controllers.console import api from controllers.console.app.error import AppNotFoundError from extensions.ext_database import db from models.model import App, RecommendedApp +from services.workflow_service import WorkflowService app_fields = { 'id': fields.String, @@ -23,8 +27,7 @@ recommended_app_fields = { 'privacy_policy': fields.String, 'category': fields.String, 'position': fields.Integer, - 'is_listed': fields.Boolean, - 'is_agent': fields.Boolean + 'is_listed': fields.Boolean } recommended_app_list_fields = { @@ -73,8 +76,7 @@ class RecommendedAppListApi(Resource): 'privacy_policy': site.privacy_policy, 'category': recommended_app.category, 'position': recommended_app.position, - 'is_listed': recommended_app.is_listed, - "is_agent": app.is_agent + 'is_listed': recommended_app.is_listed } recommended_apps_result.append(recommended_app_result) @@ -84,27 +86,6 @@ class RecommendedAppListApi(Resource): class RecommendedAppApi(Resource): - model_config_fields = { - 'opening_statement': fields.String, - 'suggested_questions': fields.Raw(attribute='suggested_questions_list'), - 'suggested_questions_after_answer': fields.Raw(attribute='suggested_questions_after_answer_dict'), - 'more_like_this': fields.Raw(attribute='more_like_this_dict'), - 'model': fields.Raw(attribute='model_dict'), - 'user_input_form': fields.Raw(attribute='user_input_form_list'), - 'pre_prompt': fields.String, - 'agent_mode': fields.Raw(attribute='agent_mode_dict'), - } - - app_simple_detail_fields = { - 'id': fields.String, - 'name': fields.String, - 'icon': fields.String, - 'icon_background': fields.String, - 'mode': fields.String, - 'app_model_config': fields.Nested(model_config_fields), - } - - @marshal_with(app_simple_detail_fields) def get(self, app_id): app_id = str(app_id) @@ -118,11 +99,38 @@ class RecommendedAppApi(Resource): raise AppNotFoundError # get app detail - app = db.session.query(App).filter(App.id == app_id).first() - if not app or not app.is_public: + app_model = db.session.query(App).filter(App.id == app_id).first() + if not app_model or not app_model.is_public: raise AppNotFoundError - return app + app_model_config = app_model.app_model_config + + export_data = { + "app": { + "name": app_model.name, + "mode": app_model.mode, + "icon": app_model.icon, + "icon_background": app_model.icon_background + }, + "model_config": app_model_config.to_dict(), + } + + if app_model_config.workflow_id: + export_data['workflow_graph'] = json.loads(app_model_config.workflow.graph) + else: + # get draft workflow + workflow_service = WorkflowService() + workflow = workflow_service.get_draft_workflow(app_model) + export_data['workflow_graph'] = json.loads(workflow.graph) + + return { + 'id': app_model.id, + 'name': app_model.name, + 'icon': app_model.icon, + 'icon_background': app_model.icon_background, + 'mode': app_model.mode, + 'export_data': yaml.dump(export_data) + } api.add_resource(RecommendedAppListApi, '/explore/apps') diff --git a/api/core/provider_manager.py b/api/core/provider_manager.py index 6e28247d38..0db84d3b69 100644 --- a/api/core/provider_manager.py +++ b/api/core/provider_manager.py @@ -235,7 +235,7 @@ class ProviderManager: if available_models: found = False for available_model in available_models: - if available_model.model == "gpt-3.5-turbo-1106": + if available_model.model == "gpt-4": default_model = TenantDefaultModel( tenant_id=tenant_id, model_type=model_type.to_origin_model_type(), diff --git a/api/fields/app_fields.py b/api/fields/app_fields.py index e6c1272086..75b68d24fc 100644 --- a/api/fields/app_fields.py +++ b/api/fields/app_fields.py @@ -42,14 +42,10 @@ app_detail_fields = { 'id': fields.String, 'name': fields.String, 'mode': fields.String, - 'is_agent': fields.Boolean, 'icon': fields.String, 'icon_background': fields.String, 'enable_site': fields.Boolean, 'enable_api': fields.Boolean, - 'api_rpm': fields.Integer, - 'api_rph': fields.Integer, - 'is_demo': fields.Boolean, 'model_config': fields.Nested(model_config_fields, attribute='app_model_config'), 'created_at': TimestampField } @@ -67,12 +63,8 @@ app_partial_fields = { 'id': fields.String, 'name': fields.String, 'mode': fields.String, - 'is_agent': fields.Boolean, 'icon': fields.String, 'icon_background': fields.String, - 'enable_site': fields.Boolean, - 'enable_api': fields.Boolean, - 'is_demo': fields.Boolean, 'model_config': fields.Nested(model_config_partial_fields, attribute='app_model_config'), 'created_at': TimestampField } @@ -122,10 +114,6 @@ app_detail_fields_with_site = { 'icon_background': fields.String, 'enable_site': fields.Boolean, 'enable_api': fields.Boolean, - 'api_rpm': fields.Integer, - 'api_rph': fields.Integer, - 'is_agent': fields.Boolean, - 'is_demo': fields.Boolean, 'model_config': fields.Nested(model_config_fields, attribute='app_model_config'), 'site': fields.Nested(site_fields), 'api_base_url': fields.String, diff --git a/api/fields/installed_app_fields.py b/api/fields/installed_app_fields.py index 821d3c0ade..35cc5a6475 100644 --- a/api/fields/installed_app_fields.py +++ b/api/fields/installed_app_fields.py @@ -17,8 +17,7 @@ installed_app_fields = { 'is_pinned': fields.Boolean, 'last_used_at': TimestampField, 'editable': fields.Boolean, - 'uninstallable': fields.Boolean, - 'is_agent': fields.Boolean, + 'uninstallable': fields.Boolean } installed_app_list_fields = { diff --git a/api/migrations/versions/b289e2408ee2_add_workflow.py b/api/migrations/versions/b289e2408ee2_add_workflow.py index 9e04fef288..7255b4b5fa 100644 --- a/api/migrations/versions/b289e2408ee2_add_workflow.py +++ b/api/migrations/versions/b289e2408ee2_add_workflow.py @@ -107,7 +107,6 @@ def upgrade(): batch_op.create_index('workflow_version_idx', ['tenant_id', 'app_id', 'version'], unique=False) with op.batch_alter_table('app_model_configs', schema=None) as batch_op: - batch_op.add_column(sa.Column('chatbot_app_engine', sa.String(length=255), server_default=sa.text("'normal'::character varying"), nullable=False)) batch_op.add_column(sa.Column('workflow_id', postgresql.UUID(), nullable=True)) with op.batch_alter_table('messages', schema=None) as batch_op: @@ -123,7 +122,6 @@ def downgrade(): with op.batch_alter_table('app_model_configs', schema=None) as batch_op: batch_op.drop_column('workflow_id') - batch_op.drop_column('chatbot_app_engine') with op.batch_alter_table('workflows', schema=None) as batch_op: batch_op.drop_index('workflow_version_idx') diff --git a/api/migrations/versions/cc04d0998d4d_set_model_config_column_nullable.py b/api/migrations/versions/cc04d0998d4d_set_model_config_column_nullable.py new file mode 100644 index 0000000000..c302e8b530 --- /dev/null +++ b/api/migrations/versions/cc04d0998d4d_set_model_config_column_nullable.py @@ -0,0 +1,70 @@ +"""set model config column nullable + +Revision ID: cc04d0998d4d +Revises: b289e2408ee2 +Create Date: 2024-02-27 03:47:47.376325 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'cc04d0998d4d' +down_revision = 'b289e2408ee2' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('app_model_configs', schema=None) as batch_op: + batch_op.alter_column('provider', + existing_type=sa.VARCHAR(length=255), + nullable=True) + batch_op.alter_column('model_id', + existing_type=sa.VARCHAR(length=255), + nullable=True) + batch_op.alter_column('configs', + existing_type=postgresql.JSON(astext_type=sa.Text()), + nullable=True) + + with op.batch_alter_table('apps', schema=None) as batch_op: + batch_op.alter_column('api_rpm', + existing_type=sa.Integer(), + server_default='0', + nullable=False) + + batch_op.alter_column('api_rph', + existing_type=sa.Integer(), + server_default='0', + nullable=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('apps', schema=None) as batch_op: + batch_op.alter_column('api_rpm', + existing_type=sa.Integer(), + server_default=None, + nullable=False) + + batch_op.alter_column('api_rph', + existing_type=sa.Integer(), + server_default=None, + nullable=False) + + with op.batch_alter_table('app_model_configs', schema=None) as batch_op: + batch_op.alter_column('configs', + existing_type=postgresql.JSON(astext_type=sa.Text()), + nullable=False) + batch_op.alter_column('model_id', + existing_type=sa.VARCHAR(length=255), + nullable=False) + batch_op.alter_column('provider', + existing_type=sa.VARCHAR(length=255), + nullable=False) + + # ### end Alembic commands ### diff --git a/api/models/model.py b/api/models/model.py index ee7146c324..fa14c5ce54 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -31,7 +31,9 @@ class AppMode(Enum): COMPLETION = 'completion' WORKFLOW = 'workflow' CHAT = 'chat' - AGENT = 'agent' + ADVANCED_CHAT = 'advanced-chat' + AGENT_CHAT = 'agent-chat' + CHANNEL = 'channel' @classmethod def value_of(cls, value: str) -> 'AppMode': @@ -64,8 +66,8 @@ class App(db.Model): status = db.Column(db.String(255), nullable=False, server_default=db.text("'normal'::character varying")) enable_site = db.Column(db.Boolean, nullable=False) enable_api = db.Column(db.Boolean, nullable=False) - api_rpm = db.Column(db.Integer, nullable=False) - api_rph = db.Column(db.Integer, nullable=False) + api_rpm = db.Column(db.Integer, nullable=False, server_default=db.text('0')) + api_rph = db.Column(db.Integer, nullable=False, server_default=db.text('0')) is_demo = db.Column(db.Boolean, nullable=False, server_default=db.text('false')) is_public = db.Column(db.Boolean, nullable=False, server_default=db.text('false')) is_universal = db.Column(db.Boolean, nullable=False, server_default=db.text('false')) @@ -92,19 +94,7 @@ class App(db.Model): def tenant(self): 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 @@ -153,11 +143,6 @@ class App(db.Model): return deleted_tools -class ChatbotAppEngine(Enum): - NORMAL = 'normal' - WORKFLOW = 'workflow' - - class AppModelConfig(db.Model): __tablename__ = 'app_model_configs' __table_args__ = ( @@ -167,9 +152,9 @@ class AppModelConfig(db.Model): id = db.Column(UUID, server_default=db.text('uuid_generate_v4()')) app_id = db.Column(UUID, nullable=False) - provider = db.Column(db.String(255), nullable=False) - model_id = db.Column(db.String(255), nullable=False) - configs = db.Column(db.JSON, nullable=False) + provider = db.Column(db.String(255), nullable=True) + model_id = db.Column(db.String(255), nullable=True) + configs = db.Column(db.JSON, nullable=True) created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)')) updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)')) opening_statement = db.Column(db.Text) @@ -191,7 +176,6 @@ class AppModelConfig(db.Model): dataset_configs = db.Column(db.Text) external_data_tools = db.Column(db.Text) file_upload = db.Column(db.Text) - chatbot_app_engine = db.Column(db.String(255), nullable=False, server_default=db.text("'normal'::character varying")) workflow_id = db.Column(UUID) @property @@ -301,9 +285,6 @@ class AppModelConfig(db.Model): def to_dict(self) -> dict: return { - "provider": "", - "model_id": "", - "configs": {}, "opening_statement": self.opening_statement, "suggested_questions": self.suggested_questions_list, "suggested_questions_after_answer": self.suggested_questions_after_answer_dict, @@ -327,9 +308,6 @@ class AppModelConfig(db.Model): } def from_model_config_dict(self, model_config: dict): - self.provider = "" - self.model_id = "" - self.configs = {} self.opening_statement = model_config['opening_statement'] self.suggested_questions = json.dumps(model_config['suggested_questions']) self.suggested_questions_after_answer = json.dumps(model_config['suggested_questions_after_answer']) @@ -358,15 +336,13 @@ class AppModelConfig(db.Model): if model_config.get('dataset_configs') else None self.file_upload = json.dumps(model_config.get('file_upload')) \ if model_config.get('file_upload') else None + self.workflow_id = model_config.get('workflow_id') return self def copy(self): new_app_model_config = AppModelConfig( id=self.id, app_id=self.app_id, - provider="", - model_id="", - configs={}, opening_statement=self.opening_statement, suggested_questions=self.suggested_questions, suggested_questions_after_answer=self.suggested_questions_after_answer, @@ -385,7 +361,8 @@ class AppModelConfig(db.Model): chat_prompt_config=self.chat_prompt_config, completion_prompt_config=self.completion_prompt_config, dataset_configs=self.dataset_configs, - file_upload=self.file_upload + file_upload=self.file_upload, + workflow_id=self.workflow_id ) return new_app_model_config @@ -446,12 +423,6 @@ class InstalledApp(db.Model): tenant = db.session.query(Tenant).filter(Tenant.id == self.tenant_id).first() return tenant - @property - def is_agent(self) -> bool: - app = self.app - if not app: - return False - return app.is_agent class Conversation(db.Model): __tablename__ = 'conversations' diff --git a/api/services/workflow/workflow_converter.py b/api/services/workflow/workflow_converter.py index c6f0bed008..ed24762dd8 100644 --- a/api/services/workflow/workflow_converter.py +++ b/api/services/workflow/workflow_converter.py @@ -21,7 +21,7 @@ from events.app_event import app_was_created from extensions.ext_database import db from models.account import Account from models.api_based_extension import APIBasedExtension, APIBasedExtensionPoint -from models.model import App, AppMode, AppModelConfig, ChatbotAppEngine, Site +from models.model import App, AppMode, AppModelConfig, Site from models.workflow import Workflow, WorkflowType @@ -85,8 +85,6 @@ class WorkflowConverter: new_app_model_config.chat_prompt_config = '' new_app_model_config.completion_prompt_config = '' new_app_model_config.dataset_configs = '' - new_app_model_config.chatbot_app_engine = ChatbotAppEngine.WORKFLOW.value \ - if app_model.mode == AppMode.CHAT.value else ChatbotAppEngine.NORMAL.value new_app_model_config.workflow_id = workflow.id db.session.add(new_app_model_config) diff --git a/api/services/workflow_service.py b/api/services/workflow_service.py index 4f7262b7d6..3143818d12 100644 --- a/api/services/workflow_service.py +++ b/api/services/workflow_service.py @@ -1,9 +1,10 @@ import json from datetime import datetime +from typing import Optional from extensions.ext_database import db from models.account import Account -from models.model import App, AppMode, ChatbotAppEngine +from models.model import App, AppMode from models.workflow import Workflow, WorkflowType from services.workflow.defaults import default_block_configs from services.workflow.workflow_converter import WorkflowConverter @@ -58,6 +59,40 @@ class WorkflowService: # return draft workflow return workflow + def publish_draft_workflow(self, app_model: App, + account: Account, + draft_workflow: Optional[Workflow] = None) -> Workflow: + """ + Publish draft workflow + + :param app_model: App instance + :param account: Account instance + :param draft_workflow: Workflow instance + """ + if not draft_workflow: + # fetch draft workflow by app_model + draft_workflow = self.get_draft_workflow(app_model=app_model) + + if not draft_workflow: + raise ValueError('No valid workflow found.') + + # create new workflow + workflow = Workflow( + tenant_id=app_model.tenant_id, + app_id=app_model.id, + type=draft_workflow.type, + version=str(datetime.utcnow()), + graph=draft_workflow.graph, + created_by=account.id + ) + + # commit db session changes + db.session.add(workflow) + db.session.commit() + + # return new workflow + return workflow + def get_default_block_configs(self) -> dict: """ Get default block configs @@ -77,11 +112,7 @@ class WorkflowService: # chatbot convert to workflow mode workflow_converter = WorkflowConverter() - if app_model.mode == AppMode.CHAT.value: - # check if chatbot app is in basic mode - if app_model.app_model_config.chatbot_app_engine != ChatbotAppEngine.NORMAL: - raise ValueError('Chatbot app already in workflow mode') - elif app_model.mode != AppMode.COMPLETION.value: + if app_model.mode not in [AppMode.CHAT.value, AppMode.COMPLETION.value]: raise ValueError(f'Current App mode: {app_model.mode} is not supported convert to workflow.') # convert to workflow