mirror of
https://github.com/langgenius/dify.git
synced 2026-05-04 09:28:04 +08:00
refactor app mode
add app import and export
This commit is contained in:
@ -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')
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
]
|
||||
|
||||
@ -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')
|
||||
|
||||
Reference in New Issue
Block a user