From fff6f7cf2fdddd799b8e5fa79d6f7b7daf5e0c10 Mon Sep 17 00:00:00 2001 From: Likalikali Date: Sun, 9 Aug 2026 16:20:10 +0800 Subject: [PATCH] refactor: replace manual model_validate with @model_validate in app workflow controllers (#40235) --- .../console/app/advanced_prompt_template.py | 15 ++-- api/controllers/console/app/agent.py | 9 +-- .../console/app/agent_app_feature.py | 14 +++- api/controllers/console/app/app_import.py | 25 +++---- api/controllers/console/app/audio.py | 20 +++--- .../console/app/conversation_variables.py | 8 +-- api/controllers/console/app/message.py | 17 ++--- api/controllers/console/app/site.py | 7 +- api/controllers/console/app/statistic.py | 51 ++++++------- .../console/app/workflow_app_log.py | 32 ++++----- .../console/app/workflow_comment.py | 71 +++++++++++++------ .../console/app/workflow_draft_variable.py | 41 ++++++----- api/controllers/console/app/workflow_run.py | 58 +++++++-------- .../console/app/workflow_statistic.py | 27 +++---- .../console/app/workflow_trigger.py | 16 ++--- .../controllers/console/app/test_app_apis.py | 35 +++++++-- .../console/app/test_app_import_api.py | 10 +-- .../controllers/console/app/test_audio.py | 20 +++--- .../app/test_conversation_variables_api.py | 14 +++- .../console/app/test_statistic_api.py | 63 +++++++++++++--- .../console/app/test_workflow_run_api.py | 12 +++- .../console/app/test_workflow_trigger_api.py | 7 +- 22 files changed, 354 insertions(+), 218 deletions(-) diff --git a/api/controllers/console/app/advanced_prompt_template.py b/api/controllers/console/app/advanced_prompt_template.py index 90098739a45..8ded3ff7234 100644 --- a/api/controllers/console/app/advanced_prompt_template.py +++ b/api/controllers/console/app/advanced_prompt_template.py @@ -1,6 +1,5 @@ from typing import Any -from flask import request from flask_restx import Resource from pydantic import BaseModel, Field @@ -10,7 +9,7 @@ from controllers.common.schema import ( register_response_schema_models, ) from controllers.console import console_ns -from controllers.console.wraps import account_initialization_required, setup_required +from controllers.console.wraps import account_initialization_required, model_validate, setup_required from fields.base import ResponseModel from libs.login import login_required from services.advanced_prompt_template_service import AdvancedPromptTemplateArgs, AdvancedPromptTemplateService @@ -49,12 +48,12 @@ class AdvancedPromptTemplateList(Resource): @setup_required @login_required @account_initialization_required - def get(self): - args = AdvancedPromptTemplateQuery.model_validate(request.args.to_dict(flat=True)) + @model_validate(AdvancedPromptTemplateQuery) + def get(self, req_data: AdvancedPromptTemplateQuery): prompt_args: AdvancedPromptTemplateArgs = { - "app_mode": args.app_mode, - "model_mode": args.model_mode, - "model_name": args.model_name, - "has_context": args.has_context, + "app_mode": req_data.app_mode, + "model_mode": req_data.model_mode, + "model_name": req_data.model_name, + "has_context": req_data.has_context, } return AdvancedPromptTemplateService.get_prompt(prompt_args) diff --git a/api/controllers/console/app/agent.py b/api/controllers/console/app/agent.py index 8be24acc8fb..325e747b3be 100644 --- a/api/controllers/console/app/agent.py +++ b/api/controllers/console/app/agent.py @@ -21,6 +21,7 @@ from controllers.console.wraps import ( RBACPermission, RBACResourceScope, account_initialization_required, + model_validate, rbac_permission_required, setup_required, with_current_tenant_id, @@ -211,12 +212,12 @@ def _upload_skill_for_app(*, session: Session, current_user: Account, app_model: def _commit_drive_file_for_app(*, session: Session, current_user: Account, app_model: App, allow_node_id: bool = True): + payload = AgentDriveFilePayload.model_validate(console_ns.payload or {}) query = query_params_from_request(AgentDriveMutationQuery) node_id = query.node_id if allow_node_id else None agent_id = _resolve_agent_id(session, app_model, node_id) if not agent_id: return _agent_not_bound() - payload = AgentDriveFilePayload.model_validate(console_ns.payload or {}) upload_file = session.scalar( select(UploadFile).where( @@ -341,11 +342,11 @@ class AgentLogApi(Resource): @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) @with_session(write=False) @get_app_model(mode=[AppMode.AGENT_CHAT]) - def get(self, session: Session, app_model: App): + @model_validate(AgentLogQuery) + def get(self, req_data: AgentLogQuery, session: Session, app_model: App): """Get agent logs""" - args = AgentLogQuery.model_validate(request.args.to_dict(flat=True)) - return AgentService.get_agent_logs(app_model, args.conversation_id, args.message_id, session) + return AgentService.get_agent_logs(app_model, req_data.conversation_id, req_data.message_id, session) @console_ns.route("/agent//skills/upload") diff --git a/api/controllers/console/app/agent_app_feature.py b/api/controllers/console/app/agent_app_feature.py index 99925727335..d88496cbb06 100644 --- a/api/controllers/console/app/agent_app_feature.py +++ b/api/controllers/console/app/agent_app_feature.py @@ -25,6 +25,7 @@ from controllers.console.wraps import ( RBACResourceScope, account_initialization_required, edit_permission_required, + model_validate, rbac_permission_required, setup_required, with_current_tenant_id, @@ -87,14 +88,21 @@ class AgentAppFeatureConfigResource(Resource): @with_current_user @with_current_tenant_id @with_session - def post(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID): + @model_validate(AgentAppFeaturesPayload) + def post( + self, + req_data: AgentAppFeaturesPayload, + session: Session, + tenant_id: str, + current_user: Account, + agent_id: UUID, + ): app_model = resolve_agent_runtime_app_model(session=session, tenant_id=tenant_id, agent_id=agent_id) - args = AgentAppFeaturesPayload.model_validate(console_ns.payload or {}) new_app_model_config = AgentAppFeatureConfigService.update_features( app_model=app_model, account=current_user, - config=args.model_dump(exclude_none=True), + config=req_data.model_dump(exclude_none=True), session=session, ) diff --git a/api/controllers/console/app/app_import.py b/api/controllers/console/app/app_import.py index c03e738bf8a..f2d3bace841 100644 --- a/api/controllers/console/app/app_import.py +++ b/api/controllers/console/app/app_import.py @@ -12,6 +12,7 @@ from controllers.console.wraps import ( account_initialization_required, cloud_edition_billing_resource_check, edit_permission_required, + model_validate, rbac_permission_required, setup_required, with_current_user, @@ -83,8 +84,8 @@ class AppImportApi(Resource): @edit_permission_required @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL, resource_required=False) @with_current_user - def post(self, current_user: Account | None = None): - args = AppImportPayload.model_validate(console_ns.payload) + @model_validate(AppImportPayload) + def post(self, req_data: AppImportPayload, current_user: Account | None = None): current_user = current_user if current_user is not None else _current_user_and_tenant_id(None)[0] # AppDslService performs internal commits for some creation paths, so use a plain @@ -96,15 +97,15 @@ class AppImportApi(Resource): try: result = import_service.import_app( account=account, - import_mode=args.mode, - yaml_content=args.yaml_content, - yaml_url=args.yaml_url, - name=args.name, - description=args.description, - icon_type=args.icon_type, - icon=args.icon, - icon_background=args.icon_background, - app_id=args.app_id, + import_mode=req_data.mode, + yaml_content=req_data.yaml_content, + yaml_url=req_data.yaml_url, + name=req_data.name, + description=req_data.description, + icon_type=req_data.icon_type, + icon=req_data.icon, + icon_background=req_data.icon_background, + app_id=req_data.app_id, ) except NoPermissionError as e: raise Forbidden(str(e)) @@ -113,7 +114,7 @@ class AppImportApi(Resource): else: session.commit() - is_created_app = args.app_id is None and result.status in { + is_created_app = req_data.app_id is None and result.status in { ImportStatus.COMPLETED, ImportStatus.COMPLETED_WITH_WARNINGS, } diff --git a/api/controllers/console/app/audio.py b/api/controllers/console/app/audio.py index 059cc96269d..b476817cf08 100644 --- a/api/controllers/console/app/audio.py +++ b/api/controllers/console/app/audio.py @@ -31,6 +31,7 @@ from controllers.console.wraps import ( RBACResourceScope, account_initialization_required, edit_permission_required, + model_validate, rbac_permission_required, setup_required, with_current_tenant_id, @@ -276,15 +277,15 @@ class ChatMessageTextApi(Resource): @login_required @account_initialization_required @get_app_model - def post(self, app_model: App): + @model_validate(TextToSpeechPayload) + def post(self, req_data: TextToSpeechPayload, app_model: App): try: - payload = TextToSpeechPayload.model_validate(console_ns.payload) message_ref = None - if payload.message_id: + if req_data.message_id: app_ref = AppRefService.create_app_ref(app_model) message_ref = AppRefService.create_message_ref( app_ref, - payload.message_id, + req_data.message_id, account_id=current_user.id, ) @@ -292,8 +293,8 @@ class ChatMessageTextApi(Resource): return AudioService.transcript_tts( app_model=app_model, session=db.session(), - text=payload.text, - voice=payload.voice, + text=req_data.text, + voice=req_data.voice, message_ref=message_ref, is_draft=True, ) @@ -339,13 +340,12 @@ class TextModesApi(Resource): @account_initialization_required @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) @get_app_model - def get(self, app_model: App): + @model_validate(TextToSpeechVoiceQuery) + def get(self, req_data: TextToSpeechVoiceQuery, app_model: App): try: - args = TextToSpeechVoiceQuery.model_validate(request.args.to_dict(flat=True)) - response = AudioService.transcript_tts_voices( tenant_id=app_model.tenant_id, - language=args.language, + language=req_data.language, ) return dump_response(TextToSpeechVoiceListResponse, response) diff --git a/api/controllers/console/app/conversation_variables.py b/api/controllers/console/app/conversation_variables.py index aa8090f0440..ff0727039aa 100644 --- a/api/controllers/console/app/conversation_variables.py +++ b/api/controllers/console/app/conversation_variables.py @@ -3,7 +3,6 @@ from __future__ import annotations from datetime import datetime from typing import Any -from flask import request from flask_restx import Resource from pydantic import BaseModel, Field, field_validator from sqlalchemy import select @@ -16,6 +15,7 @@ from controllers.console.wraps import ( RBACPermission, RBACResourceScope, account_initialization_required, + model_validate, rbac_permission_required, setup_required, ) @@ -101,15 +101,15 @@ class ConversationVariablesApi(Resource): @account_initialization_required @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT) @get_app_model(mode=AppMode.ADVANCED_CHAT) - def get(self, app_model: App): - args = ConversationVariablesQuery.model_validate(request.args.to_dict(flat=True)) + @model_validate(ConversationVariablesQuery) + def get(self, req_data: ConversationVariablesQuery, app_model: App): stmt = ( select(ConversationVariable) .where(ConversationVariable.app_id == app_model.id) .order_by(ConversationVariable.created_at) ) - stmt = stmt.where(ConversationVariable.conversation_id == args.conversation_id) + stmt = stmt.where(ConversationVariable.conversation_id == req_data.conversation_id) # NOTE: This is a temporary solution to avoid performance issues. page = 1 diff --git a/api/controllers/console/app/message.py b/api/controllers/console/app/message.py index 080772107dc..8f6c2f54464 100644 --- a/api/controllers/console/app/message.py +++ b/api/controllers/console/app/message.py @@ -28,6 +28,7 @@ from controllers.console.wraps import ( RBACResourceScope, account_initialization_required, edit_permission_required, + model_validate, rbac_permission_required, setup_required, with_current_tenant_id, @@ -320,8 +321,8 @@ class MessageFeedbackExportApi(Resource): @account_initialization_required @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) @get_app_model - def get(self, app_model: App): - args = FeedbackExportQuery.model_validate(request.args.to_dict()) + @model_validate(FeedbackExportQuery) + def get(self, req_data: FeedbackExportQuery, app_model: App): # Import the service function from services.feedback_service import FeedbackService @@ -330,12 +331,12 @@ class MessageFeedbackExportApi(Resource): export_data = FeedbackService.export_feedbacks( app_model.id, session=db.session(), - from_source=args.from_source, - rating=args.rating, - has_comment=args.has_comment, - start_date=args.start_date, - end_date=args.end_date, - format_type=args.format, + from_source=req_data.from_source, + rating=req_data.rating, + has_comment=req_data.has_comment, + start_date=req_data.start_date, + end_date=req_data.end_date, + format_type=req_data.format, ) return export_data diff --git a/api/controllers/console/app/site.py b/api/controllers/console/app/site.py index 669c59e6d53..01835769c19 100644 --- a/api/controllers/console/app/site.py +++ b/api/controllers/console/app/site.py @@ -17,6 +17,7 @@ from controllers.console.wraps import ( account_initialization_required, edit_permission_required, is_admin_or_owner_required, + model_validate, rbac_permission_required, setup_required, with_current_user, @@ -98,8 +99,8 @@ class AppSite(Resource): @with_current_user @with_session @get_app_model - def post(self, session: Session, current_user: Account, app_model: App): - args = AppSiteUpdatePayload.model_validate(console_ns.payload or {}) + @model_validate(AppSiteUpdatePayload) + def post(self, req_data: AppSiteUpdatePayload, session: Session, current_user: Account, app_model: App): site = session.scalar(select(Site).where(Site.app_id == app_model.id).limit(1)) if not site: raise NotFound @@ -123,7 +124,7 @@ class AppSite(Resource): "show_workflow_steps", "use_icon_as_answer_icon", ]: - value = getattr(args, attr_name) + value = getattr(req_data, attr_name) if value is not None: setattr(site, attr_name, value) diff --git a/api/controllers/console/app/statistic.py b/api/controllers/console/app/statistic.py index c7c3c9e64b6..48635166e3a 100644 --- a/api/controllers/console/app/statistic.py +++ b/api/controllers/console/app/statistic.py @@ -1,7 +1,7 @@ from decimal import Decimal import sqlalchemy as sa -from flask import abort, request +from flask import abort from flask_restx import Resource from pydantic import BaseModel, Field, field_validator @@ -12,6 +12,7 @@ from controllers.console.wraps import ( RBACPermission, RBACResourceScope, account_initialization_required, + model_validate, rbac_permission_required, setup_required, with_current_user, @@ -157,8 +158,8 @@ class DailyMessageStatistic(Resource): @with_current_user @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR) @get_app_model - def get(self, account: Account, app_model: App): - args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True)) + @model_validate(StatisticTimeRangeQuery) + def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App): converted_created_at = convert_datetime_to_date("created_at") sql_query = f"""SELECT @@ -177,7 +178,7 @@ WHERE } try: - start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone) + start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone) except ValueError as e: abort(400, description=str(e)) @@ -217,8 +218,8 @@ class DailyConversationStatistic(Resource): @with_current_user @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR) @get_app_model - def get(self, account: Account, app_model: App): - args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True)) + @model_validate(StatisticTimeRangeQuery) + def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App): converted_created_at = convert_datetime_to_date("created_at") sql_query = f"""SELECT @@ -237,7 +238,7 @@ WHERE } try: - start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone) + start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone) except ValueError as e: abort(400, description=str(e)) @@ -276,8 +277,8 @@ class DailyTerminalsStatistic(Resource): @with_current_user @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR) @get_app_model - def get(self, account: Account, app_model: App): - args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True)) + @model_validate(StatisticTimeRangeQuery) + def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App): converted_created_at = convert_datetime_to_date("created_at") sql_query = f"""SELECT @@ -296,7 +297,7 @@ WHERE } try: - start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone) + start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone) except ValueError as e: abort(400, description=str(e)) @@ -336,8 +337,8 @@ class DailyTokenCostStatistic(Resource): @with_current_user @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR) @get_app_model - def get(self, account: Account, app_model: App): - args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True)) + @model_validate(StatisticTimeRangeQuery) + def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App): converted_created_at = convert_datetime_to_date("created_at") sql_query = f"""SELECT @@ -357,7 +358,7 @@ WHERE } try: - start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone) + start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone) except ValueError as e: abort(400, description=str(e)) @@ -399,8 +400,8 @@ class AverageSessionInteractionStatistic(Resource): @with_current_user @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR) @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT]) - def get(self, account: Account, app_model: App): - args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True)) + @model_validate(StatisticTimeRangeQuery) + def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App): converted_created_at = convert_datetime_to_date("c.created_at") sql_query = f"""SELECT @@ -427,7 +428,7 @@ FROM } try: - start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone) + start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone) except ValueError as e: abort(400, description=str(e)) @@ -478,8 +479,8 @@ class UserSatisfactionRateStatistic(Resource): @with_current_user @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR) @get_app_model - def get(self, account: Account, app_model: App): - args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True)) + @model_validate(StatisticTimeRangeQuery) + def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App): converted_created_at = convert_datetime_to_date("m.created_at") sql_query = f"""SELECT @@ -502,7 +503,7 @@ WHERE } try: - start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone) + start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone) except ValueError as e: abort(400, description=str(e)) @@ -547,8 +548,8 @@ class AverageResponseTimeStatistic(Resource): @with_current_user @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR) @get_app_model(mode=AppMode.COMPLETION) - def get(self, account: Account, app_model: App): - args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True)) + @model_validate(StatisticTimeRangeQuery) + def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App): converted_created_at = convert_datetime_to_date("created_at") sql_query = f"""SELECT @@ -567,7 +568,7 @@ WHERE } try: - start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone) + start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone) except ValueError as e: abort(400, description=str(e)) @@ -607,8 +608,8 @@ class TokensPerSecondStatistic(Resource): @with_current_user @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR) @get_app_model - def get(self, account: Account, app_model: App): - args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True)) + @model_validate(StatisticTimeRangeQuery) + def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App): converted_created_at = convert_datetime_to_date("created_at") sql_query = f"""SELECT @@ -630,7 +631,7 @@ WHERE } try: - start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone) + start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone) except ValueError as e: abort(400, description=str(e)) diff --git a/api/controllers/console/app/workflow_app_log.py b/api/controllers/console/app/workflow_app_log.py index b3426e8f6ea..12a36939b8b 100644 --- a/api/controllers/console/app/workflow_app_log.py +++ b/api/controllers/console/app/workflow_app_log.py @@ -2,7 +2,6 @@ from datetime import datetime from typing import Any from dateutil.parser import isoparse -from flask import request from flask_restx import Resource from pydantic import BaseModel, Field, field_validator from sqlalchemy.orm import sessionmaker @@ -14,6 +13,7 @@ from controllers.console.wraps import ( RBACPermission, RBACResourceScope, account_initialization_required, + model_validate, rbac_permission_required, setup_required, ) @@ -183,11 +183,11 @@ class WorkflowAppLogApi(Resource): @account_initialization_required @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_LOG_AND_ANNOTATION) @get_app_model(mode=[AppMode.WORKFLOW]) - def get(self, app_model: App): + @model_validate(WorkflowAppLogQuery) + def get(self, req_data: WorkflowAppLogQuery, app_model: App): """ Get workflow app logs """ - args = WorkflowAppLogQuery.model_validate(request.args.to_dict(flat=True)) # get paginate workflow app logs workflow_app_service = WorkflowAppService() @@ -195,15 +195,15 @@ class WorkflowAppLogApi(Resource): workflow_app_log_pagination = workflow_app_service.get_paginate_workflow_app_logs( session=session, app_model=app_model, - keyword=args.keyword, - status=args.status, - created_at_before=args.created_at__before, - created_at_after=args.created_at__after, - page=args.page, - limit=args.limit, - detail=args.detail, - created_by_end_user_session_id=args.created_by_end_user_session_id, - created_by_account=args.created_by_account, + keyword=req_data.keyword, + status=req_data.status, + created_at_before=req_data.created_at__before, + created_at_after=req_data.created_at__after, + page=req_data.page, + limit=req_data.limit, + detail=req_data.detail, + created_by_end_user_session_id=req_data.created_by_end_user_session_id, + created_by_account=req_data.created_by_account, ) return WorkflowAppLogPaginationResponse.model_validate( @@ -227,19 +227,19 @@ class WorkflowArchivedLogApi(Resource): @account_initialization_required @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_LOG_AND_ANNOTATION) @get_app_model(mode=[AppMode.WORKFLOW]) - def get(self, app_model: App): + @model_validate(WorkflowAppLogQuery) + def get(self, req_data: WorkflowAppLogQuery, app_model: App): """ Get workflow archived logs """ - args = WorkflowAppLogQuery.model_validate(request.args.to_dict(flat=True)) workflow_app_service = WorkflowAppService() with sessionmaker(db.engine, expire_on_commit=False).begin() as session: workflow_app_log_pagination = workflow_app_service.get_paginate_workflow_archive_logs( session=session, app_model=app_model, - page=args.page, - limit=args.limit, + page=req_data.page, + limit=req_data.limit, ) return WorkflowArchivedLogPaginationResponse.model_validate( diff --git a/api/controllers/console/app/workflow_comment.py b/api/controllers/console/app/workflow_comment.py index 082b49b20f2..cc156294e86 100644 --- a/api/controllers/console/app/workflow_comment.py +++ b/api/controllers/console/app/workflow_comment.py @@ -10,6 +10,7 @@ from controllers.console.app.wraps import get_app_model from controllers.console.wraps import ( account_initialization_required, edit_permission_required, + model_validate, setup_required, with_current_tenant_id, with_current_user, @@ -239,18 +240,24 @@ class WorkflowCommentListApi(Resource): @with_current_user @with_current_tenant_id @get_app_model() - def post(self, current_tenant_id: str, current_user: Account, app_model: App): + @model_validate(WorkflowCommentCreatePayload) + def post( + self, + req_data: WorkflowCommentCreatePayload, + current_tenant_id: str, + current_user: Account, + app_model: App, + ): """Create a new workflow comment.""" - payload = WorkflowCommentCreatePayload.model_validate(console_ns.payload or {}) result = WorkflowCommentService.create_comment( tenant_id=current_tenant_id, app_id=app_model.id, created_by=current_user.id, - content=payload.content, - position_x=payload.position_x, - position_y=payload.position_y, - mentioned_user_ids=payload.mentioned_user_ids, + content=req_data.content, + position_x=req_data.position_x, + position_y=req_data.position_y, + mentioned_user_ids=req_data.mentioned_user_ids, ) return dump_response(WorkflowCommentCreate, result), 201 @@ -289,19 +296,26 @@ class WorkflowCommentDetailApi(Resource): @with_current_user @with_current_tenant_id @get_app_model() - def put(self, current_tenant_id: str, current_user: Account, app_model: App, comment_id: str): + @model_validate(WorkflowCommentUpdatePayload) + def put( + self, + req_data: WorkflowCommentUpdatePayload, + current_tenant_id: str, + current_user: Account, + app_model: App, + comment_id: str, + ): """Update a workflow comment.""" - payload = WorkflowCommentUpdatePayload.model_validate(console_ns.payload or {}) result = WorkflowCommentService.update_comment( tenant_id=current_tenant_id, app_id=app_model.id, comment_id=comment_id, user_id=current_user.id, - content=payload.content, - position_x=payload.position_x, - position_y=payload.position_y, - mentioned_user_ids=payload.mentioned_user_ids, + content=req_data.content, + position_x=req_data.position_x, + position_y=req_data.position_y, + mentioned_user_ids=req_data.mentioned_user_ids, ) return dump_response(WorkflowCommentUpdate, result) @@ -372,20 +386,26 @@ class WorkflowCommentReplyApi(Resource): @with_current_user @with_current_tenant_id @get_app_model() - def post(self, current_tenant_id: str, current_user: Account, app_model: App, comment_id: str): + @model_validate(WorkflowCommentReplyPayload) + def post( + self, + req_data: WorkflowCommentReplyPayload, + current_tenant_id: str, + current_user: Account, + app_model: App, + comment_id: str, + ): """Add a reply to a workflow comment.""" # Validate comment access first WorkflowCommentService.validate_comment_access( comment_id=comment_id, tenant_id=current_tenant_id, app_id=app_model.id ) - payload = WorkflowCommentReplyPayload.model_validate(console_ns.payload or {}) - result = WorkflowCommentService.create_reply( comment_id=comment_id, - content=payload.content, + content=req_data.content, created_by=current_user.id, - mentioned_user_ids=payload.mentioned_user_ids, + mentioned_user_ids=req_data.mentioned_user_ids, ) return dump_response(WorkflowCommentReplyCreate, result), 201 @@ -407,23 +427,30 @@ class WorkflowCommentReplyDetailApi(Resource): @with_current_user @with_current_tenant_id @get_app_model() - def put(self, current_tenant_id: str, current_user: Account, app_model: App, comment_id: str, reply_id: str): + @model_validate(WorkflowCommentReplyPayload) + def put( + self, + req_data: WorkflowCommentReplyPayload, + current_tenant_id: str, + current_user: Account, + app_model: App, + comment_id: str, + reply_id: str, + ): """Update a comment reply.""" # Validate comment access first WorkflowCommentService.validate_comment_access( comment_id=comment_id, tenant_id=current_tenant_id, app_id=app_model.id ) - payload = WorkflowCommentReplyPayload.model_validate(console_ns.payload or {}) - reply = WorkflowCommentService.update_reply( tenant_id=current_tenant_id, app_id=app_model.id, comment_id=comment_id, reply_id=reply_id, user_id=current_user.id, - content=payload.content, - mentioned_user_ids=payload.mentioned_user_ids, + content=req_data.content, + mentioned_user_ids=req_data.mentioned_user_ids, ) return dump_response(WorkflowCommentReplyUpdate, reply) diff --git a/api/controllers/console/app/workflow_draft_variable.py b/api/controllers/console/app/workflow_draft_variable.py index b960d09acd3..6d52419f88c 100644 --- a/api/controllers/console/app/workflow_draft_variable.py +++ b/api/controllers/console/app/workflow_draft_variable.py @@ -4,7 +4,7 @@ from functools import wraps from typing import Any, Concatenate, Self, TypedDict, override from uuid import UUID -from flask import Response, request +from flask import Response from flask_restx import Resource, fields, marshal, marshal_with from pydantic import BaseModel, ConfigDict, Field, model_validator from sqlalchemy.orm import sessionmaker @@ -22,6 +22,7 @@ from controllers.console.wraps import ( RBACResourceScope, account_initialization_required, edit_permission_required, + model_validate, rbac_permission_required, setup_required, with_current_user, @@ -359,11 +360,11 @@ class WorkflowVariableCollectionApi(Resource): @_api_prerequisite @marshal_with(workflow_draft_variable_list_without_value_model) @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) - def get(self, current_user: Account, app_model: App): + @model_validate(WorkflowDraftVariableListQuery) + def get(self, req_data: WorkflowDraftVariableListQuery, current_user: Account, app_model: App): """ Get draft workflow """ - args = WorkflowDraftVariableListQuery.model_validate(request.args.to_dict(flat=True)) # fetch draft workflow by app_model workflow_service = WorkflowService() @@ -378,8 +379,8 @@ class WorkflowVariableCollectionApi(Resource): ) workflow_vars = draft_var_srv.list_variables_without_values( app_id=app_model.id, - page=args.page, - limit=args.limit, + page=req_data.page, + limit=req_data.limit, user_id=current_user.id, ) @@ -479,7 +480,14 @@ class VariableApi(Resource): @console_ns.response(404, "Variable not found") @_api_prerequisite @marshal_with(workflow_draft_variable_model) - def patch(self, current_user: Account, app_model: App, variable_id: UUID): + @model_validate(WorkflowDraftVariableUpdatePayload) + def patch( + self, + req_data: WorkflowDraftVariableUpdatePayload, + current_user: Account, + app_model: App, + variable_id: UUID, + ): # Request payload for file types: # # Local File: @@ -504,7 +512,6 @@ class VariableApi(Resource): draft_var_srv = WorkflowDraftVariableService( session=db.session(), ) - args_model = WorkflowDraftVariableUpdatePayload.model_validate(console_ns.payload or {}) variable_id_str = str(variable_id) variable = ensure_variable_access( @@ -514,8 +521,8 @@ class VariableApi(Resource): current_user_id=current_user.id, ) - new_name = args_model.name - raw_value = args_model.value + new_name = req_data.name + raw_value = req_data.value if new_name is None and raw_value is None: return variable @@ -660,13 +667,13 @@ class ConversationVariableCollectionApi(Resource): @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) @with_current_user @get_app_model(mode=AppMode.ADVANCED_CHAT) - def post(self, current_user: Account, app_model: App): - payload = ConversationVariableUpdatePayload.model_validate(console_ns.payload or {}) + @model_validate(ConversationVariableUpdatePayload) + def post(self, req_data: ConversationVariableUpdatePayload, current_user: Account, app_model: App): workflow_service = WorkflowService() conversation_variables_list = [ - variable.model_dump(mode="json", exclude_unset=True) for variable in payload.conversation_variables + variable.model_dump(mode="json", exclude_unset=True) for variable in req_data.conversation_variables ] conversation_variables = [ variable_factory.build_conversation_variable_from_mapping(obj) for obj in conversation_variables_list @@ -755,24 +762,24 @@ class EnvironmentVariableCollectionApi(Resource): @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) @with_current_user @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) - def post(self, current_user: Account, app_model: App): - payload = EnvironmentVariableUpdatePayload.model_validate(console_ns.payload or {}) + @model_validate(EnvironmentVariableUpdatePayload) + def post(self, req_data: EnvironmentVariableUpdatePayload, current_user: Account, app_model: App): workflow_service = WorkflowService() environment_variables_list = [ - variable.model_dump(mode="json", exclude_unset=True) for variable in payload.environment_variables + variable.model_dump(mode="json", exclude_unset=True) for variable in req_data.environment_variables ] environment_variables = [ variable_factory.build_environment_variable_from_mapping(obj) for obj in environment_variables_list ] - if payload.patch: + if req_data.patch: workflow_service.patch_draft_workflow_environment_variables( app_model=app_model, account=current_user, environment_variables=environment_variables, - deleted_environment_variable_ids=payload.deleted_environment_variable_ids, + deleted_environment_variable_ids=req_data.deleted_environment_variable_ids, session=db.session(), ) else: diff --git a/api/controllers/console/app/workflow_run.py b/api/controllers/console/app/workflow_run.py index a71aa444e27..bc842dbf6b4 100644 --- a/api/controllers/console/app/workflow_run.py +++ b/api/controllers/console/app/workflow_run.py @@ -2,7 +2,6 @@ from datetime import UTC, datetime, timedelta from typing import Literal from uuid import UUID -from flask import request from flask_restx import Resource from pydantic import BaseModel, Field, field_validator from sqlalchemy import select @@ -17,6 +16,7 @@ from controllers.console.wraps import ( RBACPermission, RBACResourceScope, account_initialization_required, + model_validate, rbac_permission_required, setup_required, with_current_tenant_id, @@ -160,21 +160,21 @@ class AdvancedChatAppWorkflowRunListApi(Resource): @account_initialization_required @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT) @get_app_model(mode=[AppMode.ADVANCED_CHAT]) - def get(self, app_model: App): + @model_validate(WorkflowRunListQuery) + def get(self, req_data: WorkflowRunListQuery, app_model: App): """ Get advanced chat app workflow run list """ - args_model = WorkflowRunListQuery.model_validate(request.args.to_dict(flat=True)) - args: WorkflowRunListArgs = {"limit": args_model.limit} - if args_model.last_id is not None: - args["last_id"] = args_model.last_id - if args_model.status is not None: - args["status"] = args_model.status + args: WorkflowRunListArgs = {"limit": req_data.limit} + if req_data.last_id is not None: + args["last_id"] = req_data.last_id + if req_data.status is not None: + args["status"] = req_data.status # Default to DEBUGGING if not specified triggered_from = ( - WorkflowRunTriggeredFrom(args_model.triggered_from) - if args_model.triggered_from + WorkflowRunTriggeredFrom(req_data.triggered_from) + if req_data.triggered_from else WorkflowRunTriggeredFrom.DEBUGGING ) @@ -258,17 +258,17 @@ class AdvancedChatAppWorkflowRunCountApi(Resource): @account_initialization_required @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT) @get_app_model(mode=[AppMode.ADVANCED_CHAT]) - def get(self, app_model: App): + @model_validate(WorkflowRunCountQuery) + def get(self, req_data: WorkflowRunCountQuery, app_model: App): """ Get advanced chat workflow runs count statistics """ - args_model = WorkflowRunCountQuery.model_validate(request.args.to_dict(flat=True)) - args = args_model.model_dump(exclude_none=True) + args = req_data.model_dump(exclude_none=True) # Default to DEBUGGING if not specified triggered_from = ( - WorkflowRunTriggeredFrom(args_model.triggered_from) - if args_model.triggered_from + WorkflowRunTriggeredFrom(req_data.triggered_from) + if req_data.triggered_from else WorkflowRunTriggeredFrom.DEBUGGING ) @@ -299,21 +299,21 @@ class WorkflowRunListApi(Resource): @account_initialization_required @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) - def get(self, app_model: App): + @model_validate(WorkflowRunListQuery) + def get(self, req_data: WorkflowRunListQuery, app_model: App): """ Get workflow run list """ - args_model = WorkflowRunListQuery.model_validate(request.args.to_dict(flat=True)) - args: WorkflowRunListArgs = {"limit": args_model.limit} - if args_model.last_id is not None: - args["last_id"] = args_model.last_id - if args_model.status is not None: - args["status"] = args_model.status + args: WorkflowRunListArgs = {"limit": req_data.limit} + if req_data.last_id is not None: + args["last_id"] = req_data.last_id + if req_data.status is not None: + args["status"] = req_data.status # Default to DEBUGGING for workflow if not specified (backward compatibility) triggered_from = ( - WorkflowRunTriggeredFrom(args_model.triggered_from) - if args_model.triggered_from + WorkflowRunTriggeredFrom(req_data.triggered_from) + if req_data.triggered_from else WorkflowRunTriggeredFrom.DEBUGGING ) @@ -341,17 +341,17 @@ class WorkflowRunCountApi(Resource): @account_initialization_required @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) - def get(self, app_model: App): + @model_validate(WorkflowRunCountQuery) + def get(self, req_data: WorkflowRunCountQuery, app_model: App): """ Get workflow runs count statistics """ - args_model = WorkflowRunCountQuery.model_validate(request.args.to_dict(flat=True)) - args = args_model.model_dump(exclude_none=True) + args = req_data.model_dump(exclude_none=True) # Default to DEBUGGING for workflow if not specified (backward compatibility) triggered_from = ( - WorkflowRunTriggeredFrom(args_model.triggered_from) - if args_model.triggered_from + WorkflowRunTriggeredFrom(req_data.triggered_from) + if req_data.triggered_from else WorkflowRunTriggeredFrom.DEBUGGING ) diff --git a/api/controllers/console/app/workflow_statistic.py b/api/controllers/console/app/workflow_statistic.py index 0346d510fbc..e72cd057a65 100644 --- a/api/controllers/console/app/workflow_statistic.py +++ b/api/controllers/console/app/workflow_statistic.py @@ -1,4 +1,4 @@ -from flask import abort, jsonify, request +from flask import abort, jsonify from flask_restx import Resource from pydantic import BaseModel, Field, field_validator from sqlalchemy.orm import sessionmaker @@ -10,6 +10,7 @@ from controllers.console.wraps import ( RBACPermission, RBACResourceScope, account_initialization_required, + model_validate, rbac_permission_required, setup_required, with_current_user, @@ -104,13 +105,13 @@ class WorkflowDailyRunsStatistic(Resource): @with_current_user @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR) @get_app_model - def get(self, account: Account, app_model: App): - args = WorkflowStatisticQuery.model_validate(request.args.to_dict(flat=True)) + @model_validate(WorkflowStatisticQuery) + def get(self, req_data: WorkflowStatisticQuery, account: Account, app_model: App): assert account.timezone is not None try: - start_date, end_date = parse_time_range(args.start, args.end, account.timezone) + start_date, end_date = parse_time_range(req_data.start, req_data.end, account.timezone) except ValueError as e: abort(400, description=str(e)) @@ -148,13 +149,13 @@ class WorkflowDailyTerminalsStatistic(Resource): @with_current_user @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR) @get_app_model - def get(self, account: Account, app_model: App): - args = WorkflowStatisticQuery.model_validate(request.args.to_dict(flat=True)) + @model_validate(WorkflowStatisticQuery) + def get(self, req_data: WorkflowStatisticQuery, account: Account, app_model: App): assert account.timezone is not None try: - start_date, end_date = parse_time_range(args.start, args.end, account.timezone) + start_date, end_date = parse_time_range(req_data.start, req_data.end, account.timezone) except ValueError as e: abort(400, description=str(e)) @@ -192,13 +193,13 @@ class WorkflowDailyTokenCostStatistic(Resource): @with_current_user @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR) @get_app_model - def get(self, account: Account, app_model: App): - args = WorkflowStatisticQuery.model_validate(request.args.to_dict(flat=True)) + @model_validate(WorkflowStatisticQuery) + def get(self, req_data: WorkflowStatisticQuery, account: Account, app_model: App): assert account.timezone is not None try: - start_date, end_date = parse_time_range(args.start, args.end, account.timezone) + start_date, end_date = parse_time_range(req_data.start, req_data.end, account.timezone) except ValueError as e: abort(400, description=str(e)) @@ -236,13 +237,13 @@ class WorkflowAverageAppInteractionStatistic(Resource): @with_current_user @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR) @get_app_model(mode=[AppMode.WORKFLOW]) - def get(self, account: Account, app_model: App): - args = WorkflowStatisticQuery.model_validate(request.args.to_dict(flat=True)) + @model_validate(WorkflowStatisticQuery) + def get(self, req_data: WorkflowStatisticQuery, account: Account, app_model: App): assert account.timezone is not None try: - start_date, end_date = parse_time_range(args.start, args.end, account.timezone) + start_date, end_date = parse_time_range(req_data.start, req_data.end, account.timezone) except ValueError as e: abort(400, description=str(e)) diff --git a/api/controllers/console/app/workflow_trigger.py b/api/controllers/console/app/workflow_trigger.py index 2f45d637256..4f544db7c46 100644 --- a/api/controllers/console/app/workflow_trigger.py +++ b/api/controllers/console/app/workflow_trigger.py @@ -1,7 +1,6 @@ import logging from datetime import datetime -from flask import request from flask_restx import Resource from pydantic import BaseModel, field_validator from sqlalchemy import select @@ -25,6 +24,7 @@ from ..wraps import ( RBACResourceScope, account_initialization_required, edit_permission_required, + model_validate, rbac_permission_required, setup_required, with_current_tenant_id, @@ -102,11 +102,11 @@ class WebhookTriggerApi(Resource): @console_ns.response(200, "Success", console_ns.models[WebhookTriggerResponse.__name__]) @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) @get_app_model(mode=AppMode.WORKFLOW) - def get(self, app_model: App): + @model_validate(Parser) + def get(self, req_data: Parser, app_model: App): """Get webhook trigger for a node""" - args = Parser.model_validate(request.args.to_dict(flat=True)) - node_id = args.node_id + node_id = req_data.node_id with sessionmaker(db.engine, expire_on_commit=False).begin() as session: # Get webhook trigger for this app and node @@ -175,11 +175,11 @@ class AppTriggerEnableApi(Resource): @console_ns.response(200, "Success", console_ns.models[WorkflowTriggerResponse.__name__]) @with_current_tenant_id @get_app_model(mode=AppMode.WORKFLOW) - def post(self, current_tenant_id: str, app_model: App): + @model_validate(ParserEnable) + def post(self, req_data: ParserEnable, current_tenant_id: str, app_model: App): """Update app trigger (enable/disable)""" - args = ParserEnable.model_validate(console_ns.payload) - trigger_id = args.trigger_id + trigger_id = req_data.trigger_id with sessionmaker(db.engine, expire_on_commit=False).begin() as session: # Find the trigger using select trigger = session.execute( @@ -194,7 +194,7 @@ class AppTriggerEnableApi(Resource): raise NotFound("Trigger not found") # Update status based on enable_trigger boolean - trigger.status = AppTriggerStatus.ENABLED if args.enable_trigger else AppTriggerStatus.DISABLED + trigger.status = AppTriggerStatus.ENABLED if req_data.enable_trigger else AppTriggerStatus.DISABLED # Add computed icon field url_prefix = dify_config.CONSOLE_API_URL + "/console/api/workspaces/current/tool-provider/builtin/" diff --git a/api/tests/unit_tests/controllers/console/app/test_app_apis.py b/api/tests/unit_tests/controllers/console/app/test_app_apis.py index 1089fe87312..165eaad2633 100644 --- a/api/tests/unit_tests/controllers/console/app/test_app_apis.py +++ b/api/tests/unit_tests/controllers/console/app/test_app_apis.py @@ -59,6 +59,7 @@ from controllers.console.app.workflow import AdvancedChatWorkflowRunPayload, Syn from controllers.console.app.workflow_app_log import WorkflowAppLogQuery from controllers.console.app.workflow_draft_variable import ( EnvironmentVariableUpdatePayload, + WorkflowDraftVariableListQuery, WorkflowDraftVariableUpdatePayload, ) from controllers.console.app.workflow_statistic import WorkflowStatisticQuery @@ -421,7 +422,13 @@ class TestSiteEndpoints: site = self._add_site(db.session) with database_app.test_request_context("/", json={"title": "My Site", "input_placeholder": "Ask me anything"}): - result = method(api, db.session, _make_account(), app_model=_make_app()) + result = method( + api, + AppSiteUpdatePayload(title="My Site", input_placeholder="Ask me anything"), + db.session, + _make_account(), + app_model=_make_app(), + ) db.session.refresh(site) assert isinstance(result, dict) @@ -488,7 +495,7 @@ class TestWorkflowAppLogEndpoints: ) with database_app.test_request_context("/?page=1&limit=20"): - result = method(api, app_model=_make_app("app-1")) + result = method(api, WorkflowAppLogQuery(page=1, limit=20), app_model=_make_app("app-1")) assert result == {"page": 1, "limit": 20, "total": 0, "has_more": False, "data": []} @@ -518,7 +525,12 @@ class TestWorkflowDraftVariableEndpoints: monkeypatch.setattr(workflow_draft_variable_module, "WorkflowService", DummyWorkflowService) with database_app.test_request_context("/?page=1&limit=20"): - result = method(api, _make_account(), app_model=_make_app("app-1")) + result = method( + api, + WorkflowDraftVariableListQuery(page=1, limit=20), + _make_account(), + app_model=_make_app("app-1"), + ) assert result == {"items": [], "total": 0} @@ -571,7 +583,16 @@ class TestWorkflowDraftVariableEndpoints: "deleted_environment_variable_ids": ["env-b"], }, ): - result = method(api, _make_account(), app_model=_make_app()) + result = method( + api, + EnvironmentVariableUpdatePayload( + environment_variables=[{"id": "env-a", "name": "a", "value_type": "string", "value": "new-a"}], + patch=True, + deleted_environment_variable_ids=["env-b"], + ), + _make_account(), + app_model=_make_app(), + ) assert result == {"result": "success"} assert [(variable.id, variable.value) for variable in captured["environment_variables"]] == [("env-a", "new-a")] @@ -617,7 +638,7 @@ class TestWorkflowStatisticEndpoints: with database_app.test_request_context("/"): account = _make_account() account.timezone = "UTC" - response = method(api, account, app_model=_make_app("app-1", tenant_id="t1")) + response = method(api, WorkflowStatisticQuery(), account, app_model=_make_app("app-1", tenant_id="t1")) assert response.get_json() == {"data": [{"date": "2024-01-01"}]} @@ -647,7 +668,7 @@ class TestWorkflowStatisticEndpoints: with database_app.test_request_context("/"): account = _make_account() account.timezone = "UTC" - response = method(api, account, app_model=_make_app("app-1", tenant_id="t1")) + response = method(api, WorkflowStatisticQuery(), account, app_model=_make_app("app-1", tenant_id="t1")) assert response.get_json() == {"data": [{"date": "2024-01-02"}]} @@ -677,7 +698,7 @@ class TestWorkflowTriggerEndpoints: db.session.commit() with database_app.test_request_context("/?node_id=node-1"): - result = method(api, app_model=_make_app()) + result = method(api, Parser(node_id="node-1"), app_model=_make_app()) assert isinstance(result, dict) assert {"id", "webhook_id", "webhook_url", "webhook_debug_url", "node_id", "created_at"} <= set(result.keys()) diff --git a/api/tests/unit_tests/controllers/console/app/test_app_import_api.py b/api/tests/unit_tests/controllers/console/app/test_app_import_api.py index 4068ec0e7f3..b92f75f408e 100644 --- a/api/tests/unit_tests/controllers/console/app/test_app_import_api.py +++ b/api/tests/unit_tests/controllers/console/app/test_app_import_api.py @@ -159,7 +159,7 @@ class TestAppImportApi: ) with app.test_request_context("/console/api/apps/imports", method="POST", json={"mode": "yaml-content"}): - response, status = method(api, _make_account()) + response, status = method(api, app_import_module.AppImportPayload(mode="yaml-content"), _make_account()) assert transaction_events.rollbacks == 1 assert transaction_events.commits == 0 @@ -185,7 +185,7 @@ class TestAppImportApi: ) with app.test_request_context("/console/api/apps/imports", method="POST", json={"mode": "yaml-content"}): - response, status = method(api, _make_account()) + response, status = method(api, app_import_module.AppImportPayload(mode="yaml-content"), _make_account()) assert transaction_events.commits == 1 assert transaction_events.rollbacks == 0 @@ -213,7 +213,7 @@ class TestAppImportApi: monkeypatch.setattr(app_import_module.EnterpriseService.WebAppAuth, "update_app_access_mode", update_access) with app.test_request_context("/console/api/apps/imports", method="POST", json={"mode": "yaml-content"}): - response, status = method(api, _make_account()) + response, status = method(api, app_import_module.AppImportPayload(mode="yaml-content"), _make_account()) assert transaction_events.commits == 1 assert transaction_events.rollbacks == 0 @@ -251,7 +251,7 @@ class TestAppImportApi: ) with app.test_request_context("/console/api/apps/imports", method="POST", json={"mode": "yaml-content"}): - response, status = method() + response, status = method(app_import_module.AppImportPayload(mode="yaml-content")) assert transaction_events.commits == 1 _assert_app_persistence(sqlite_app_engine, app_id, persisted=True) @@ -291,7 +291,7 @@ class TestAppImportApi: method="POST", json={"mode": "yaml-content", "app_id": "existing-app"}, ): - response, status = method() + response, status = method(app_import_module.AppImportPayload(mode="yaml-content", app_id="existing-app")) assert transaction_events.commits == 1 _assert_app_persistence(sqlite_app_engine, app_id, persisted=True) diff --git a/api/tests/unit_tests/controllers/console/app/test_audio.py b/api/tests/unit_tests/controllers/console/app/test_audio.py index 8b661aa2646..347627809ff 100644 --- a/api/tests/unit_tests/controllers/console/app/test_audio.py +++ b/api/tests/unit_tests/controllers/console/app/test_audio.py @@ -17,6 +17,8 @@ from controllers.console.app.audio import ( ChatMessageAudioApi, ChatMessageTextApi, TextModesApi, + TextToSpeechPayload, + TextToSpeechVoiceQuery, ) from controllers.console.app.error import ( AppUnavailableError, @@ -290,7 +292,7 @@ def test_console_text_api_success(app: Flask, monkeypatch: pytest.MonkeyPatch) - method="POST", json={"text": "hello", "voice": "v"}, ): - response = handler(api, app_model=app_model) + response = handler(api, TextToSpeechPayload(text="hello"), app_model=app_model) assert response == {"audio": "ok"} @@ -315,7 +317,7 @@ def test_console_text_api_builds_message_ref(app: Flask, monkeypatch: pytest.Mon ), patch("controllers.console.app.audio.current_user", SimpleNamespace(id="account-1")), ): - response = handler(api, app_model=app_model) + response = handler(api, TextToSpeechPayload(text="hello", message_id="message-1"), app_model=app_model) assert response == {"audio": "ok"} assert calls["message_ref"] == MessageRef(AppRef("tenant-1", "app-1"), "message-1", account_id="account-1") @@ -334,7 +336,7 @@ def test_console_text_api_error_mapping(app: Flask, monkeypatch: pytest.MonkeyPa json={"text": "hello"}, ): with pytest.raises(ProviderQuotaExceededError): - handler(api, app_model=app_model) + handler(api, TextToSpeechPayload(text="hello"), app_model=app_model) def test_console_text_modes_success(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: @@ -346,7 +348,7 @@ def test_console_text_modes_success(app: Flask, monkeypatch: pytest.MonkeyPatch) app_model = SimpleNamespace(tenant_id="t1") with app.test_request_context("/console/api/apps/app/text-to-audio/voices?language=en", method="GET"): - response = handler(api, app_model=app_model) + response = handler(api, TextToSpeechVoiceQuery(language="en-US"), app_model=app_model) assert response == expected_voices @@ -364,7 +366,7 @@ def test_console_text_modes_language_error(app: Flask, monkeypatch: pytest.Monke with app.test_request_context("/console/api/apps/app/text-to-audio/voices?language=en", method="GET"): with pytest.raises(AppUnavailableError): - handler(api, app_model=app_model) + handler(api, TextToSpeechVoiceQuery(language="en-US"), app_model=app_model) def test_audio_to_text_success(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: @@ -424,7 +426,7 @@ def test_text_to_audio_success(app: Flask, monkeypatch: pytest.MonkeyPatch) -> N method="POST", json={"text": "hello"}, ): - response = method(api, app_model=app_model) + response = method(api, TextToSpeechPayload(text="hello"), app_model=app_model) assert response == {"audio": "ok"} @@ -443,7 +445,7 @@ def test_text_to_audio_voices_success(app: Flask, monkeypatch: pytest.MonkeyPatc method="GET", query_string={"language": "en-US"}, ): - response = method(api, app_model=app_model) + response = method(api, TextToSpeechVoiceQuery(language="en-US"), app_model=app_model) assert response == expected_voices @@ -481,7 +483,7 @@ def test_text_to_audio_with_language_param(app: Flask, monkeypatch: pytest.Monke method="POST", json={"text": "hello", "language": "en-US"}, ): - response = method(api, app_model=app_model) + response = method(api, TextToSpeechPayload(text="hello"), app_model=app_model) assert response == {"audio": "test"} @@ -501,5 +503,5 @@ def test_text_to_audio_voices_with_language_filter(app: Flask, monkeypatch: pyte "/console/api/apps/app-1/text-to-audio/voices?language=en-US", method="GET", ): - response = method(api, app_model=app_model) + response = method(api, TextToSpeechVoiceQuery(language="en-US"), app_model=app_model) assert isinstance(response, list) diff --git a/api/tests/unit_tests/controllers/console/app/test_conversation_variables_api.py b/api/tests/unit_tests/controllers/console/app/test_conversation_variables_api.py index 688309b0281..f44358e6897 100644 --- a/api/tests/unit_tests/controllers/console/app/test_conversation_variables_api.py +++ b/api/tests/unit_tests/controllers/console/app/test_conversation_variables_api.py @@ -51,7 +51,11 @@ def test_get_conversation_variables_returns_paginated_response( method="GET", query_string={"conversation_id": "conv-1"}, ): - response = method(api, app_model=SimpleNamespace(id="app-1")) + response = method( + api, + conversation_variables_module.ConversationVariablesQuery(conversation_id="conv-1"), + app_model=SimpleNamespace(id="app-1"), + ) assert response["page"] == 1 assert response["limit"] == 100 @@ -90,7 +94,11 @@ def test_get_conversation_variables_normalizes_value_type_and_value( method="GET", query_string={"conversation_id": "conv-1"}, ): - response = method(api, app_model=SimpleNamespace(id="app-1")) + response = method( + api, + conversation_variables_module.ConversationVariablesQuery(conversation_id="conv-1"), + app_model=SimpleNamespace(id="app-1"), + ) assert response["data"][0]["value_type"] == "number" assert response["data"][0]["value"] == "42" @@ -102,4 +110,4 @@ def test_get_conversation_variables_requires_conversation_id(app) -> None: with app.test_request_context("/console/api/apps/app-1/conversation-variables", method="GET"): with pytest.raises(ValidationError): - method(api, app_model=SimpleNamespace(id="app-1")) + conversation_variables_module.ConversationVariablesQuery.model_validate({}) diff --git a/api/tests/unit_tests/controllers/console/app/test_statistic_api.py b/api/tests/unit_tests/controllers/console/app/test_statistic_api.py index c51a38ad798..8afb4762e8c 100644 --- a/api/tests/unit_tests/controllers/console/app/test_statistic_api.py +++ b/api/tests/unit_tests/controllers/console/app/test_statistic_api.py @@ -53,7 +53,12 @@ def test_daily_message_statistic_returns_rows(app: Flask, monkeypatch: pytest.Mo _install_db(monkeypatch, rows) with app.test_request_context("/console/api/apps/app-1/statistics/daily-messages", method="GET"): - response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1")) + response = method( + api, + SimpleNamespace(start=None, end=None), + SimpleNamespace(timezone="UTC"), + app_model=SimpleNamespace(id="app-1"), + ) assert _json_payload(response) == {"data": [{"date": "2024-01-01", "message_count": 3}]} @@ -67,7 +72,12 @@ def test_daily_conversation_statistic_returns_rows(app: Flask, monkeypatch: pyte _install_db(monkeypatch, rows) with app.test_request_context("/console/api/apps/app-1/statistics/daily-conversations", method="GET"): - response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1")) + response = method( + api, + SimpleNamespace(start=None, end=None), + SimpleNamespace(timezone="UTC"), + app_model=SimpleNamespace(id="app-1"), + ) assert _json_payload(response) == {"data": [{"date": "2024-01-02", "conversation_count": 5}]} @@ -81,7 +91,12 @@ def test_daily_token_cost_statistic_returns_rows(app: Flask, monkeypatch: pytest _install_db(monkeypatch, rows) with app.test_request_context("/console/api/apps/app-1/statistics/token-costs", method="GET"): - response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1")) + response = method( + api, + SimpleNamespace(start=None, end=None), + SimpleNamespace(timezone="UTC"), + app_model=SimpleNamespace(id="app-1"), + ) data = _json_payload(response) assert len(data["data"]) == 1 @@ -99,7 +114,12 @@ def test_daily_terminals_statistic_returns_rows(app: Flask, monkeypatch: pytest. _install_db(monkeypatch, rows) with app.test_request_context("/console/api/apps/app-1/statistics/daily-end-users", method="GET"): - response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1")) + response = method( + api, + SimpleNamespace(start=None, end=None), + SimpleNamespace(timezone="UTC"), + app_model=SimpleNamespace(id="app-1"), + ) assert _json_payload(response) == {"data": [{"date": "2024-01-04", "terminal_count": 7}]} @@ -126,7 +146,12 @@ def test_daily_message_statistic_with_invalid_time_range(app: Flask, monkeypatch with app.test_request_context("/console/api/apps/app-1/statistics/daily-messages", method="GET"): with pytest.raises(BadRequest): - method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1")) + method( + api, + SimpleNamespace(start=None, end=None), + SimpleNamespace(timezone="UTC"), + app_model=SimpleNamespace(id="app-1"), + ) def test_daily_message_statistic_multiple_rows(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: @@ -142,7 +167,12 @@ def test_daily_message_statistic_multiple_rows(app: Flask, monkeypatch: pytest.M _install_db(monkeypatch, rows) with app.test_request_context("/console/api/apps/app-1/statistics/daily-messages", method="GET"): - response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1")) + response = method( + api, + SimpleNamespace(start=None, end=None), + SimpleNamespace(timezone="UTC"), + app_model=SimpleNamespace(id="app-1"), + ) data = _json_payload(response) assert len(data["data"]) == 3 @@ -156,7 +186,12 @@ def test_daily_message_statistic_empty_result(app: Flask, monkeypatch: pytest.Mo _install_db(monkeypatch, []) with app.test_request_context("/console/api/apps/app-1/statistics/daily-messages", method="GET"): - response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1")) + response = method( + api, + SimpleNamespace(start=None, end=None), + SimpleNamespace(timezone="UTC"), + app_model=SimpleNamespace(id="app-1"), + ) assert _json_payload(response) == {"data": []} @@ -175,7 +210,12 @@ def test_daily_conversation_statistic_with_time_range(app: Flask, monkeypatch: p monkeypatch.setattr(statistic_module, "convert_datetime_to_date", lambda field: field) with app.test_request_context("/console/api/apps/app-1/statistics/daily-conversations", method="GET"): - response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1")) + response = method( + api, + SimpleNamespace(start=None, end=None), + SimpleNamespace(timezone="UTC"), + app_model=SimpleNamespace(id="app-1"), + ) assert _json_payload(response) == {"data": [{"date": "2024-01-02", "conversation_count": 5}]} @@ -192,7 +232,12 @@ def test_daily_token_cost_with_multiple_currencies(app: Flask, monkeypatch: pyte _install_db(monkeypatch, rows) with app.test_request_context("/console/api/apps/app-1/statistics/token-costs", method="GET"): - response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1")) + response = method( + api, + SimpleNamespace(start=None, end=None), + SimpleNamespace(timezone="UTC"), + app_model=SimpleNamespace(id="app-1"), + ) data = _json_payload(response) assert len(data["data"]) == 2 diff --git a/api/tests/unit_tests/controllers/console/app/test_workflow_run_api.py b/api/tests/unit_tests/controllers/console/app/test_workflow_run_api.py index 71034ebd405..4986331e5e7 100644 --- a/api/tests/unit_tests/controllers/console/app/test_workflow_run_api.py +++ b/api/tests/unit_tests/controllers/console/app/test_workflow_run_api.py @@ -98,7 +98,11 @@ def test_workflow_run_list_returns_frontend_history_contract(app: Flask, monkeyp handler = unwrap(api.get) with app.test_request_context("/apps/app-1/workflow-runs?limit=10", method="GET"): - payload = handler(api, app_model=SimpleNamespace(id="app-1", tenant_id="tenant-1")) + payload = handler( + api, + workflow_run_module.WorkflowRunListQuery(limit=10), + app_model=SimpleNamespace(id="app-1", tenant_id="tenant-1"), + ) response = _serialize_200_response(api.get, payload) @@ -139,7 +143,11 @@ def test_advanced_chat_workflow_run_list_keeps_message_fields(app: Flask, monkey handler = unwrap(api.get) with app.test_request_context("/apps/app-1/advanced-chat/workflow-runs?limit=1", method="GET"): - payload = handler(api, app_model=SimpleNamespace(id="app-1", tenant_id="tenant-1")) + payload = handler( + api, + workflow_run_module.WorkflowRunListQuery(limit=1), + app_model=SimpleNamespace(id="app-1", tenant_id="tenant-1"), + ) response = _serialize_200_response(api.get, payload) diff --git a/api/tests/unit_tests/controllers/console/app/test_workflow_trigger_api.py b/api/tests/unit_tests/controllers/console/app/test_workflow_trigger_api.py index 848d57c87c0..cb1ce0cf87f 100644 --- a/api/tests/unit_tests/controllers/console/app/test_workflow_trigger_api.py +++ b/api/tests/unit_tests/controllers/console/app/test_workflow_trigger_api.py @@ -142,7 +142,12 @@ def test_app_trigger_enable_uses_injected_tenant_id(app: Flask, database_session app.test_request_context("/", json=payload), patch.object(type(console_ns), "payload", new_callable=PropertyMock, return_value=payload), ): - response = method(api, app_model.tenant_id, app_model) + response = method( + api, + workflow_trigger_module.ParserEnable(trigger_id=trigger.id, enable_trigger=True), + app_model.tenant_id, + app_model, + ) assert response["id"] == trigger.id assert response["status"] == "enabled"