refactor app mode

add app import and export
This commit is contained in:
takatost
2024-02-27 13:23:01 +08:00
parent 61b4bedc16
commit 6e3cd62e31
15 changed files with 371 additions and 688 deletions

View File

@ -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/<uuid:app_id>')
api.add_resource(AppCopy, '/apps/<uuid:app_id>/copy')
api.add_resource(AppExportApi, '/apps/<uuid:app_id>/export')
api.add_resource(AppNameApi, '/apps/<uuid:app_id>/name')
api.add_resource(AppIconApi, '/apps/<uuid:app_id>/icon')
api.add_resource(AppSiteStatus, '/apps/<uuid:app_id>/site-enable')

View File

@ -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

View File

@ -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)

View File

@ -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
]

View File

@ -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')