diff --git a/api/controllers/console/agent/composer.py b/api/controllers/console/agent/composer.py index c48818519ab..db52ba11e27 100644 --- a/api/controllers/console/agent/composer.py +++ b/api/controllers/console/agent/composer.py @@ -1,6 +1,5 @@ from uuid import UUID -from flask import request from flask_restx import Resource from sqlalchemy.orm import Session from werkzeug.exceptions import NotFound @@ -14,6 +13,7 @@ from controllers.console.wraps import ( RBACResourceScope, account_initialization_required, edit_permission_required, + model_validate, rbac_permission_required, setup_required, with_current_tenant_id, @@ -64,8 +64,16 @@ class WorkflowAgentComposerApi(Resource): @with_current_tenant_id @with_session @get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT]) - def get(self, session: Session, tenant_id: str, account_id: str, app_model: App, node_id: str): - query = WorkflowAgentComposerQuery.model_validate(request.args.to_dict(flat=True)) + @model_validate(WorkflowAgentComposerQuery) + def get( + self, + req_data: WorkflowAgentComposerQuery, + session: Session, + tenant_id: str, + account_id: str, + app_model: App, + node_id: str, + ): return dump_response( WorkflowAgentComposerResponse, AgentComposerService.load_workflow_composer( @@ -74,7 +82,7 @@ class WorkflowAgentComposerApi(Resource): app_id=app_model.id, node_id=node_id, account_id=account_id, - snapshot_id=query.snapshot_id, + snapshot_id=req_data.snapshot_id, ), ) @@ -91,8 +99,16 @@ class WorkflowAgentComposerApi(Resource): @with_current_tenant_id @with_session @get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT]) - def put(self, session: Session, tenant_id: str, account_id: str, app_model: App, node_id: str): - payload = ComposerSavePayload.model_validate(console_ns.payload or {}) + @model_validate(ComposerSavePayload) + def put( + self, + req_data: ComposerSavePayload, + session: Session, + tenant_id: str, + account_id: str, + app_model: App, + node_id: str, + ): return dump_response( WorkflowAgentComposerResponse, AgentComposerService.save_workflow_composer( @@ -101,7 +117,7 @@ class WorkflowAgentComposerApi(Resource): app_id=app_model.id, node_id=node_id, account_id=account_id, - payload=payload, + payload=req_data, ), ) @@ -123,8 +139,16 @@ class WorkflowAgentComposerCopyFromRosterApi(Resource): @with_current_tenant_id @with_session @get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT]) - def post(self, session: Session, tenant_id: str, account_id: str, app_model: App, node_id: str): - payload = WorkflowComposerCopyFromRosterPayload.model_validate(console_ns.payload or {}) + @model_validate(WorkflowComposerCopyFromRosterPayload) + def post( + self, + req_data: WorkflowComposerCopyFromRosterPayload, + session: Session, + tenant_id: str, + account_id: str, + app_model: App, + node_id: str, + ): return dump_response( WorkflowAgentComposerResponse, AgentComposerService.copy_workflow_composer_from_roster( @@ -133,9 +157,9 @@ class WorkflowAgentComposerCopyFromRosterApi(Resource): app_id=app_model.id, node_id=node_id, account_id=account_id, - source_agent_id=payload.source_agent_id, - source_snapshot_id=payload.source_snapshot_id, - idempotency_key=payload.idempotency_key, + source_agent_id=req_data.source_agent_id, + source_snapshot_id=req_data.source_snapshot_id, + idempotency_key=req_data.idempotency_key, ), ) @@ -152,16 +176,16 @@ class WorkflowAgentComposerValidateApi(Resource): @with_current_tenant_id @with_session(write=False) @get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT]) - def post(self, session: Session, tenant_id: str, app_model: App, node_id: str): - payload = ComposerSavePayload.model_validate(console_ns.payload or {}) - ComposerConfigValidator.validate_publish_payload(payload) + @model_validate(ComposerSavePayload) + def post(self, req_data: ComposerSavePayload, session: Session, tenant_id: str, app_model: App, node_id: str): + ComposerConfigValidator.validate_publish_payload(req_data) AgentComposerService.validate_knowledge_datasets( - session=session, tenant_id=tenant_id, agent_soul=payload.agent_soul + session=session, tenant_id=tenant_id, agent_soul=req_data.agent_soul ) findings = AgentComposerService.collect_validation_findings( session=session, tenant_id=tenant_id, - payload=payload, + payload=req_data, agent_id=AgentComposerService.resolve_workflow_node_agent_id( session=session, tenant_id=tenant_id, app_id=app_model.id, node_id=node_id ), @@ -204,9 +228,9 @@ class WorkflowAgentComposerImpactApi(Resource): @with_current_tenant_id @with_session(write=False) @get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT]) - def post(self, session: Session, tenant_id: str, app_model: App, node_id: str): - payload = ComposerSavePayload.model_validate(console_ns.payload or {}) - current_snapshot_id = payload.binding.current_snapshot_id if payload.binding else None + @model_validate(ComposerSavePayload) + def post(self, req_data: ComposerSavePayload, session: Session, tenant_id: str, app_model: App, node_id: str): + current_snapshot_id = req_data.binding.current_snapshot_id if req_data.binding else None if not current_snapshot_id: return dump_response( AgentComposerImpactResponse, {"current_snapshot_id": None, "workflow_node_count": 0, "bindings": []} @@ -235,8 +259,16 @@ class WorkflowAgentComposerSaveToRosterApi(Resource): @with_current_tenant_id @with_session @get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT]) - def post(self, session: Session, tenant_id: str, account_id: str, app_model: App, node_id: str): - payload = ComposerSavePayload.model_validate(console_ns.payload or {}) + @model_validate(ComposerSavePayload) + def post( + self, + req_data: ComposerSavePayload, + session: Session, + tenant_id: str, + account_id: str, + app_model: App, + node_id: str, + ): return dump_response( WorkflowAgentComposerResponse, AgentComposerService.save_workflow_composer( @@ -245,7 +277,7 @@ class WorkflowAgentComposerSaveToRosterApi(Resource): app_id=app_model.id, node_id=node_id, account_id=account_id, - payload=payload, + payload=req_data, ), ) @@ -270,8 +302,16 @@ class SnippetAgentComposerApi(Resource): @with_current_user_id @with_current_tenant_id @with_session - def get(self, session: Session, tenant_id: str, account_id: str, snippet_id: UUID, node_id: str): - query = WorkflowAgentComposerQuery.model_validate(request.args.to_dict(flat=True)) + @model_validate(WorkflowAgentComposerQuery) + def get( + self, + req_data: WorkflowAgentComposerQuery, + session: Session, + tenant_id: str, + account_id: str, + snippet_id: UUID, + node_id: str, + ): return dump_response( WorkflowAgentComposerResponse, AgentComposerService.load_workflow_composer( @@ -280,7 +320,7 @@ class SnippetAgentComposerApi(Resource): app_id=_require_snippet_app_id(session=session, tenant_id=tenant_id, snippet_id=snippet_id), node_id=node_id, account_id=account_id, - snapshot_id=query.snapshot_id, + snapshot_id=req_data.snapshot_id, ), ) @@ -296,8 +336,16 @@ class SnippetAgentComposerApi(Resource): @with_current_user_id @with_current_tenant_id @with_session - def put(self, session: Session, tenant_id: str, account_id: str, snippet_id: UUID, node_id: str): - payload = ComposerSavePayload.model_validate(console_ns.payload or {}) + @model_validate(ComposerSavePayload) + def put( + self, + req_data: ComposerSavePayload, + session: Session, + tenant_id: str, + account_id: str, + snippet_id: UUID, + node_id: str, + ): return dump_response( WorkflowAgentComposerResponse, AgentComposerService.save_workflow_composer( @@ -306,7 +354,7 @@ class SnippetAgentComposerApi(Resource): app_id=_require_snippet_app_id(session=session, tenant_id=tenant_id, snippet_id=snippet_id), node_id=node_id, account_id=account_id, - payload=payload, + payload=req_data, ), ) @@ -327,8 +375,16 @@ class SnippetAgentComposerCopyFromRosterApi(Resource): @with_current_user_id @with_current_tenant_id @with_session - def post(self, session: Session, tenant_id: str, account_id: str, snippet_id: UUID, node_id: str): - payload = WorkflowComposerCopyFromRosterPayload.model_validate(console_ns.payload or {}) + @model_validate(WorkflowComposerCopyFromRosterPayload) + def post( + self, + req_data: WorkflowComposerCopyFromRosterPayload, + session: Session, + tenant_id: str, + account_id: str, + snippet_id: UUID, + node_id: str, + ): return dump_response( WorkflowAgentComposerResponse, AgentComposerService.copy_workflow_composer_from_roster( @@ -337,9 +393,9 @@ class SnippetAgentComposerCopyFromRosterApi(Resource): app_id=_require_snippet_app_id(session=session, tenant_id=tenant_id, snippet_id=snippet_id), node_id=node_id, account_id=account_id, - source_agent_id=payload.source_agent_id, - source_snapshot_id=payload.source_snapshot_id, - idempotency_key=payload.idempotency_key, + source_agent_id=req_data.source_agent_id, + source_snapshot_id=req_data.source_snapshot_id, + idempotency_key=req_data.idempotency_key, ), ) @@ -355,17 +411,17 @@ class SnippetAgentComposerValidateApi(Resource): @account_initialization_required @with_current_tenant_id @with_session(write=False) - def post(self, session: Session, tenant_id: str, snippet_id: UUID, node_id: str): + @model_validate(ComposerSavePayload) + def post(self, req_data: ComposerSavePayload, session: Session, tenant_id: str, snippet_id: UUID, node_id: str): app_id = _require_snippet_app_id(session=session, tenant_id=tenant_id, snippet_id=snippet_id) - payload = ComposerSavePayload.model_validate(console_ns.payload or {}) - ComposerConfigValidator.validate_publish_payload(payload) + ComposerConfigValidator.validate_publish_payload(req_data) AgentComposerService.validate_knowledge_datasets( - session=session, tenant_id=tenant_id, agent_soul=payload.agent_soul + session=session, tenant_id=tenant_id, agent_soul=req_data.agent_soul ) findings = AgentComposerService.collect_validation_findings( session=session, tenant_id=tenant_id, - payload=payload, + payload=req_data, agent_id=AgentComposerService.resolve_workflow_node_agent_id( session=session, tenant_id=tenant_id, @@ -409,10 +465,10 @@ class SnippetAgentComposerImpactApi(Resource): @account_initialization_required @with_current_tenant_id @with_session(write=False) - def post(self, session: Session, tenant_id: str, snippet_id: UUID, node_id: str): + @model_validate(ComposerSavePayload) + def post(self, req_data: ComposerSavePayload, session: Session, tenant_id: str, snippet_id: UUID, node_id: str): _require_snippet_app_id(session=session, tenant_id=tenant_id, snippet_id=snippet_id) - payload = ComposerSavePayload.model_validate(console_ns.payload or {}) - current_snapshot_id = payload.binding.current_snapshot_id if payload.binding else None + current_snapshot_id = req_data.binding.current_snapshot_id if req_data.binding else None if not current_snapshot_id: return dump_response( AgentComposerImpactResponse, {"current_snapshot_id": None, "workflow_node_count": 0, "bindings": []} @@ -444,8 +500,16 @@ class SnippetAgentComposerSaveToRosterApi(Resource): @with_current_user_id @with_current_tenant_id @with_session - def post(self, session: Session, tenant_id: str, account_id: str, snippet_id: UUID, node_id: str): - payload = ComposerSavePayload.model_validate(console_ns.payload or {}) + @model_validate(ComposerSavePayload) + def post( + self, + req_data: ComposerSavePayload, + session: Session, + tenant_id: str, + account_id: str, + snippet_id: UUID, + node_id: str, + ): return dump_response( WorkflowAgentComposerResponse, AgentComposerService.save_workflow_composer( @@ -454,7 +518,7 @@ class SnippetAgentComposerSaveToRosterApi(Resource): app_id=_require_snippet_app_id(session=session, tenant_id=tenant_id, snippet_id=snippet_id), node_id=node_id, account_id=account_id, - payload=payload, + payload=req_data, ), ) @@ -484,8 +548,8 @@ class AgentComposerApi(Resource): @with_current_user_id @with_current_tenant_id @with_session - def put(self, session: Session, tenant_id: str, account_id: str, agent_id: UUID): - payload = ComposerSavePayload.model_validate(console_ns.payload or {}) + @model_validate(ComposerSavePayload) + def put(self, req_data: ComposerSavePayload, session: Session, tenant_id: str, account_id: str, agent_id: UUID): return dump_response( AgentAppComposerResponse, AgentComposerService.save_agent_composer( @@ -493,7 +557,7 @@ class AgentComposerApi(Resource): tenant_id=tenant_id, agent_id=str(agent_id), account_id=account_id, - payload=payload, + payload=req_data, ), ) @@ -509,17 +573,17 @@ class AgentComposerValidateApi(Resource): @account_initialization_required @with_current_tenant_id @with_session - def post(self, session: Session, tenant_id: str, agent_id: UUID): + @model_validate(ComposerSavePayload) + def post(self, req_data: ComposerSavePayload, session: Session, tenant_id: str, agent_id: UUID): AgentComposerService.load_agent_composer(session=session, tenant_id=tenant_id, agent_id=str(agent_id)) - payload = ComposerSavePayload.model_validate(console_ns.payload or {}) - ComposerConfigValidator.validate_publish_payload(payload) + ComposerConfigValidator.validate_publish_payload(req_data) AgentComposerService.validate_knowledge_datasets( - session=session, tenant_id=tenant_id, agent_soul=payload.agent_soul + session=session, tenant_id=tenant_id, agent_soul=req_data.agent_soul ) findings = AgentComposerService.collect_validation_findings( session=session, tenant_id=tenant_id, - payload=payload, + payload=req_data, agent_id=str(agent_id), ) return dump_response(AgentComposerValidateResponse, {"result": "success", "errors": [], **findings}) diff --git a/api/controllers/console/agent/roster.py b/api/controllers/console/agent/roster.py index 8e233c81db8..aed038eed7e 100644 --- a/api/controllers/console/agent/roster.py +++ b/api/controllers/console/agent/roster.py @@ -38,6 +38,7 @@ from controllers.console.wraps import ( edit_permission_required, enterprise_license_required, is_admin_or_owner_required, + model_validate, rbac_permission_required, setup_required, with_current_tenant_id, @@ -607,16 +608,16 @@ class AgentAppListApi(Resource): @with_current_user @with_current_tenant_id @with_session - def post(self, session: Session, current_tenant_id: str, current_user: Account): - args = AgentAppCreatePayload.model_validate(console_ns.payload) + @model_validate(AgentAppCreatePayload) + def post(self, req_data: AgentAppCreatePayload, session: Session, current_tenant_id: str, current_user: Account): params = CreateAppParams( - name=args.name, - description=args.description, + name=req_data.name, + description=req_data.description, mode="agent", - agent_role=args.role or "", - icon_type=args.icon_type, - icon=args.icon, - icon_background=args.icon_background, + agent_role=req_data.role or "", + icon_type=req_data.icon_type, + icon=req_data.icon, + icon_background=req_data.icon_background, ) app = AppService().create_app(current_tenant_id, params, current_user, session=session) @@ -649,18 +650,25 @@ class AgentAppApi(Resource): @with_current_user @with_current_tenant_id @with_session - def put(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID): + @model_validate(AgentAppUpdatePayload) + def put( + self, + req_data: AgentAppUpdatePayload, + session: Session, + tenant_id: str, + current_user: Account, + agent_id: UUID, + ): app_model = _resolve_agent_app_model(session, tenant_id=tenant_id, agent_id=agent_id) - args = AgentAppUpdatePayload.model_validate(console_ns.payload) args_dict: AppService.ArgsDict = { - "name": args.name, - "description": args.description or "", - "icon_type": args.icon_type, - "icon": args.icon or "", - "icon_background": args.icon_background or "", - "use_icon_as_answer_icon": args.use_icon_as_answer_icon or False, - "max_active_requests": args.max_active_requests or 0, - "role": args.role, + "name": req_data.name, + "description": req_data.description or "", + "icon_type": req_data.icon_type, + "icon": req_data.icon or "", + "icon_background": req_data.icon_background or "", + "use_icon_as_answer_icon": req_data.use_icon_as_answer_icon or False, + "max_active_requests": req_data.max_active_requests or 0, + "role": req_data.role, } updated = AppService().update_app(app_model, args_dict, session=session) return _serialize_agent_app_detail(session, updated, current_user=current_user) @@ -721,14 +729,21 @@ class AgentPublishApi(Resource): @with_current_user @with_current_tenant_id @with_session - def post(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID): - args = AgentPublishPayload.model_validate(console_ns.payload or {}) + @model_validate(AgentPublishPayload) + def post( + self, + req_data: AgentPublishPayload, + session: Session, + tenant_id: str, + current_user: Account, + agent_id: UUID, + ): return AgentComposerService.publish_agent_app_draft( session=session, tenant_id=tenant_id, agent_id=str(agent_id), account_id=current_user.id, - version_note=args.version_note, + version_note=req_data.version_note, ) @@ -744,14 +759,21 @@ class AgentBuildDraftCheckoutApi(Resource): @with_current_user @with_current_tenant_id @with_session(write=False) - def post(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID): - args = AgentBuildDraftCheckoutPayload.model_validate(console_ns.payload or {}) + @model_validate(AgentBuildDraftCheckoutPayload) + def post( + self, + req_data: AgentBuildDraftCheckoutPayload, + session: Session, + tenant_id: str, + current_user: Account, + agent_id: UUID, + ): return AgentComposerService.checkout_agent_app_build_draft( session=session, tenant_id=tenant_id, agent_id=str(agent_id), account_id=current_user.id, - force=args.force, + force=req_data.force, ) @@ -783,14 +805,21 @@ class AgentBuildDraftApi(Resource): @with_current_user @with_current_tenant_id @with_session - def put(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID): - payload = ComposerSavePayload.model_validate(console_ns.payload or {}) + @model_validate(ComposerSavePayload) + def put( + self, + req_data: ComposerSavePayload, + session: Session, + tenant_id: str, + current_user: Account, + agent_id: UUID, + ): return AgentComposerService.save_agent_app_build_draft( session=session, tenant_id=tenant_id, agent_id=str(agent_id), account_id=current_user.id, - payload=payload, + payload=req_data, ) @console_ns.response(200, "Agent build draft discarded", console_ns.models[AgentSimpleResultResponse.__name__]) @@ -844,18 +873,25 @@ class AgentAppCopyApi(Resource): @with_current_user @with_current_tenant_id @with_session - def post(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID): - args = AgentAppCopyPayload.model_validate(console_ns.payload or {}) + @model_validate(AgentAppCopyPayload) + def post( + self, + req_data: AgentAppCopyPayload, + session: Session, + tenant_id: str, + current_user: Account, + agent_id: UUID, + ): copied_app = _agent_roster_service(session).duplicate_agent_app( tenant_id=tenant_id, agent_id=str(agent_id), account=current_user, - name=args.name, - description=args.description, - role=args.role, - icon_type=args.icon_type, - icon=args.icon, - icon_background=args.icon_background, + name=req_data.name, + description=req_data.description, + role=req_data.role, + icon_type=req_data.icon_type, + icon=req_data.icon, + icon_background=req_data.icon_background, ) return _serialize_agent_app_detail(session, copied_app, current_user=current_user), 201 @@ -887,10 +923,10 @@ class AgentApiStatusApi(Resource): @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION) @with_current_tenant_id @with_session - def post(self, session: Session, tenant_id: str, agent_id: UUID): + @model_validate(AgentApiStatusPayload) + def post(self, req_data: AgentApiStatusPayload, session: Session, tenant_id: str, agent_id: UUID): app_model = _resolve_agent_app_model(session, tenant_id=tenant_id, agent_id=agent_id) - args = AgentApiStatusPayload.model_validate(console_ns.payload) - app_model = AppService().update_app_api_status(app_model, args.enable_api, session=session) + app_model = AppService().update_app_api_status(app_model, req_data.enable_api, session=session) return _serialize_agent_api_access(session, app_model) @@ -958,16 +994,16 @@ class AgentInviteOptionsApi(Resource): @account_initialization_required @with_current_tenant_id @with_session(write=False) - def get(self, session: Session, tenant_id: str): - query = AgentInviteOptionsQuery.model_validate(request.args.to_dict(flat=True)) + @model_validate(AgentInviteOptionsQuery) + def get(self, req_data: AgentInviteOptionsQuery, session: Session, tenant_id: str): return dump_response( AgentInviteOptionsResponse, _agent_roster_service(session).list_invite_options( tenant_id=tenant_id, - page=query.page, - limit=query.limit, - keyword=query.keyword, - app_id=query.app_id, + page=req_data.page, + limit=req_data.limit, + keyword=req_data.keyword, + app_id=req_data.app_id, ), ) @@ -1082,16 +1118,23 @@ class AgentStatisticsSummaryApi(Resource): @with_current_user @with_current_tenant_id @with_session(write=False) - def get(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID): + @model_validate(AgentStatisticsQuery) + def get( + self, + req_data: AgentStatisticsQuery, + session: Session, + tenant_id: str, + current_user: Account, + agent_id: UUID, + ): app_model = _resolve_agent_runtime_app_model(session, tenant_id=tenant_id, agent_id=agent_id) - query = AgentStatisticsQuery.model_validate(request.args.to_dict(flat=True)) timezone = current_user.timezone or "UTC" - start, end = _parse_observability_time_range(query.start, query.end, current_user) + start, end = _parse_observability_time_range(req_data.start, req_data.end, current_user) try: payload = _agent_observability_service(session).get_statistics_summary( app=app_model, agent_id=str(agent_id), - params=AgentStatisticsQueryParams(source=query.source, start=start, end=end, timezone=timezone), + params=AgentStatisticsQueryParams(source=req_data.source, start=start, end=end, timezone=timezone), ) except ValueError as exc: abort(400, description=str(exc)) diff --git a/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py b/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py index 7dfa3eb7106..f824ff9258b 100644 --- a/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py +++ b/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py @@ -28,21 +28,29 @@ from controllers.console.agent.roster import ( AgentApiKeyApi, AgentApiKeyListApi, AgentApiStatusApi, + AgentApiStatusPayload, AgentAppApi, AgentAppCopyApi, + AgentAppCopyPayload, + AgentAppCreatePayload, AgentAppListApi, + AgentAppUpdatePayload, AgentBuildDraftApi, AgentBuildDraftApplyApi, AgentBuildDraftCheckoutApi, + AgentBuildDraftCheckoutPayload, AgentDebugConversationRefreshApi, AgentInviteOptionsApi, + AgentInviteOptionsQuery, AgentLogMessagesApi, AgentLogsApi, AgentLogSourcesApi, AgentPublishApi, + AgentPublishPayload, AgentRosterVersionDetailApi, AgentRosterVersionRestoreApi, AgentRosterVersionsApi, + AgentStatisticsQuery, AgentStatisticsSummaryApi, ) from controllers.console.app import completion as completion_controller @@ -59,7 +67,13 @@ from core.app.entities.app_invoke_entities import InvokeFrom from models.agent import Agent, AgentConfigDraftType, AgentScope, AgentSource, AgentStatus from models.enums import ConversationFromSource from models.model import AppMode, Conversation, Message -from services.entities.agent_entities import ComposerSaveStrategy, ComposerVariant +from services.entities.agent_entities import ( + ComposerSavePayload, + ComposerSaveStrategy, + ComposerVariant, + WorkflowAgentComposerQuery, + WorkflowComposerCopyFromRosterPayload, +) def _persist_conversation_message( @@ -430,7 +444,13 @@ def test_agent_app_list_and_create_use_agent_route( json={"name": "Iris", "description": "Agent app", "role": "Coordinator", "icon_type": "emoji", "icon": "robot"}, ): created, status = unwrap(AgentAppListApi.post)( - AgentAppListApi(), MagicMock(), "tenant-1", SimpleNamespace(id=account_id) + AgentAppListApi(), + AgentAppCreatePayload( + name="Iris", description="Agent app", role="Coordinator", icon_type="emoji", icon="robot" + ), + MagicMock(), + "tenant-1", + SimpleNamespace(id=account_id), ) assert status == 201 assert created["id"] == "agent-created" @@ -484,7 +504,13 @@ def test_agent_app_create_omits_optional_role_as_empty_string( "/console/api/agent", json={"name": "No-role Iris", "description": "Agent app", "icon_type": "emoji", "icon": "robot"}, ): - created, status = unwrap(AgentAppListApi.post)(AgentAppListApi(), MagicMock(), "tenant-1", current_user) + created, status = unwrap(AgentAppListApi.post)( + AgentAppListApi(), + AgentAppCreatePayload(name="No-role Iris", description="Agent app", icon_type="emoji", icon="robot"), + MagicMock(), + "tenant-1", + current_user, + ) assert status == 201 assert created == {"id": "agent-created", "app_id": "app-created"} create_call = cast(dict[str, object], captured["create"]) @@ -566,7 +592,14 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id( "/console/api/agent/00000000-0000-0000-0000-000000000001", json={"name": "Renamed", "description": "", "role": "Reviewer", "icon_type": "emoji", "icon": "R"}, ): - updated = unwrap(AgentAppApi.put)(AgentAppApi(), session, tenant_id, SimpleNamespace(id=account_id), agent_id) + updated = unwrap(AgentAppApi.put)( + AgentAppApi(), + AgentAppUpdatePayload(name="Renamed", description="", role="Reviewer", icon_type="emoji", icon="R"), + session, + tenant_id, + SimpleNamespace(id=account_id), + agent_id, + ) assert updated["name"] == "Renamed" assert updated["id"] == agent_id assert updated["app_id"] == app_id @@ -615,7 +648,19 @@ def test_agent_app_copy_uses_agent_id_and_returns_agent_detail( }, ): copied, status = unwrap(AgentAppCopyApi.post)( - AgentAppCopyApi(), MagicMock(), "tenant-1", current_user, agent_id + AgentAppCopyApi(), + AgentAppCopyPayload( + name="Iris copy", + description="Copied", + role="Copied role", + icon_type="emoji", + icon="sparkles", + icon_background="#fff", + ), + MagicMock(), + "tenant-1", + current_user, + agent_id, ) assert status == 201 assert copied == {"id": "copied-agent", "app_id": "copied-app", "name": "Iris"} @@ -715,7 +760,14 @@ def test_agent_publish_and_build_draft_routes_call_composer_service( with app.test_request_context( "/console/api/agent/00000000-0000-0000-0000-000000000001/publish", json={"version_note": "publish v1"} ): - published = unwrap(AgentPublishApi.post)(AgentPublishApi(), MagicMock(), "tenant-1", current_user, agent_id) + published = unwrap(AgentPublishApi.post)( + AgentPublishApi(), + AgentPublishPayload(version_note="publish v1"), + MagicMock(), + "tenant-1", + current_user, + agent_id, + ) assert published["active_config_snapshot_id"] == "version-1" captured["publish"].pop("session", None) assert captured["publish"] == { @@ -728,7 +780,12 @@ def test_agent_publish_and_build_draft_routes_call_composer_service( "/console/api/agent/00000000-0000-0000-0000-000000000001/build-draft/checkout", json={"force": True} ): checked_out = unwrap(AgentBuildDraftCheckoutApi.post)( - AgentBuildDraftCheckoutApi(), MagicMock(), "tenant-1", current_user, agent_id + AgentBuildDraftCheckoutApi(), + AgentBuildDraftCheckoutPayload(force=True), + MagicMock(), + "tenant-1", + current_user, + agent_id, ) assert checked_out["draft"]["id"] == "build-draft-1" captured["checkout"].pop("session", None) @@ -747,7 +804,17 @@ def test_agent_publish_and_build_draft_routes_call_composer_service( "/console/api/agent/00000000-0000-0000-0000-000000000001/build-draft", json={"variant": "agent_app", "save_strategy": "save_to_current_version", "agent_soul": {}}, ): - saved = unwrap(AgentBuildDraftApi.put)(AgentBuildDraftApi(), MagicMock(), "tenant-1", current_user, agent_id) + saved = unwrap(AgentBuildDraftApi.put)( + AgentBuildDraftApi(), + ComposerSavePayload( + variant=ComposerVariant.AGENT_APP, + save_strategy=ComposerSaveStrategy.SAVE_TO_CURRENT_VERSION, + ), + MagicMock(), + "tenant-1", + current_user, + agent_id, + ) assert saved["draft"]["id"] == "build-draft-1" assert captured["save"]["tenant_id"] == "tenant-1" assert captured["save"]["agent_id"] == agent_id @@ -864,7 +931,9 @@ def test_agent_api_status_and_key_routes_resolve_backing_app( with app.test_request_context( "/console/api/agent/00000000-0000-0000-0000-000000000001/api-enable", json={"enable_api": True} ): - enabled = unwrap(AgentApiStatusApi.post)(AgentApiStatusApi(), unbound_session, "tenant-1", agent_id) + enabled = unwrap(AgentApiStatusApi.post)( + AgentApiStatusApi(), AgentApiStatusPayload(enable_api=True), unbound_session, "tenant-1", agent_id + ) assert enabled["enabled"] is True assert captured["enable"] == {"app": app_model, "enable_api": True} keys = unwrap(AgentApiKeyListApi.get)(AgentApiKeyListApi(), unbound_session, "tenant-1", agent_id) @@ -948,7 +1017,12 @@ def test_agent_app_update_allows_empty_role(app: Flask, monkeypatch: pytest.Monk json={"name": "Renamed", "description": "", "role": "", "icon_type": "emoji", "icon": "R"}, ): updated = unwrap(AgentAppApi.put)( - AgentAppApi(), MagicMock(), "tenant-1", SimpleNamespace(id="account-1"), agent_id + AgentAppApi(), + AgentAppUpdatePayload(name="Renamed", description="", role="", icon_type="emoji", icon="R"), + MagicMock(), + "tenant-1", + SimpleNamespace(id="account-1"), + agent_id, ) assert updated["role"] == "" update_call = cast(dict[str, object], captured["update"]) @@ -964,7 +1038,9 @@ def test_invite_options_get_parses_app_id(app: Flask, monkeypatch: pytest.Monkey monkeypatch.setattr(roster_controller.AgentRosterService, "list_invite_options", list_invite_options) with app.test_request_context("/console/api/agent/invite-options?page=1&limit=10&app_id=app-1"): - result = unwrap(AgentInviteOptionsApi.get)(AgentInviteOptionsApi(), MagicMock(), "tenant-1") + result = unwrap(AgentInviteOptionsApi.get)( + AgentInviteOptionsApi(), AgentInviteOptionsQuery(page=1, limit=10, app_id="app-1"), MagicMock(), "tenant-1" + ) assert result == {"data": [], "page": 1, "limit": 10, "total": 0, "has_more": False} assert captured == {"tenant_id": "tenant-1", "page": 1, "limit": 10, "keyword": None, "app_id": "app-1"} @@ -1201,7 +1277,7 @@ def test_agent_observability_routes_resolve_app_from_agent_id( "/console/api/agent/00000000-0000-0000-0000-000000000001/statistics/summary?source=api" ): statistics = unwrap(AgentStatisticsSummaryApi.get)( - AgentStatisticsSummaryApi(), MagicMock(), "tenant-1", account, agent_id + AgentStatisticsSummaryApi(), AgentStatisticsQuery(source="api"), MagicMock(), "tenant-1", account, agent_id ) assert statistics["summary"]["total_messages"] == 1 stats_call = cast(dict[str, object], captured["statistics"]) @@ -1253,18 +1329,40 @@ def test_workflow_composer_get_put_validate_candidates_impact_and_save( ) with app.test_request_context("?snapshot_id=preview-version"): workflow_state = unwrap(WorkflowAgentComposerApi.get)( - WorkflowAgentComposerApi(), MagicMock(), "tenant-1", account_id, app_model, "node-1" + WorkflowAgentComposerApi(), + WorkflowAgentComposerQuery(snapshot_id="preview-version"), + MagicMock(), + "tenant-1", + account_id, + app_model, + "node-1", ) assert workflow_state["node_id"] == "node-1" assert captured_load["account_id"] == account_id assert captured_load["snapshot_id"] == "preview-version" + composer_save_payload = ComposerSavePayload( + variant=ComposerVariant.WORKFLOW, + save_strategy=ComposerSaveStrategy.NODE_JOB_ONLY, + binding={"binding_type": "roster_agent", "current_snapshot_id": "version-1"}, + ) with app.test_request_context(json=payload): saved_state = unwrap(WorkflowAgentComposerApi.put)( - WorkflowAgentComposerApi(), MagicMock(), "tenant-1", account_id, app_model, "node-1" + WorkflowAgentComposerApi(), + composer_save_payload, + MagicMock(), + "tenant-1", + account_id, + app_model, + "node-1", ) assert saved_state["save_options"] == ["node_job_only"] assert unwrap(WorkflowAgentComposerValidateApi.post)( - WorkflowAgentComposerValidateApi(), MagicMock(), "tenant-1", app_model, "node-1" + WorkflowAgentComposerValidateApi(), + composer_save_payload, + MagicMock(), + "tenant-1", + app_model, + "node-1", ) == {"result": "success", "errors": [], "warnings": [], "knowledge_retrieval_placeholder": []} assert ( unwrap(WorkflowAgentComposerCandidatesApi.get)( @@ -1274,10 +1372,21 @@ def test_workflow_composer_get_put_validate_candidates_impact_and_save( ) with app.test_request_context(json=payload): assert unwrap(WorkflowAgentComposerImpactApi.post)( - WorkflowAgentComposerImpactApi(), MagicMock(), "tenant-1", app_model, "node-1" + WorkflowAgentComposerImpactApi(), + composer_save_payload, + MagicMock(), + "tenant-1", + app_model, + "node-1", ) == {"current_snapshot_id": "version-1", "workflow_node_count": 1, "bindings": []} assert unwrap(WorkflowAgentComposerSaveToRosterApi.post)( - WorkflowAgentComposerSaveToRosterApi(), MagicMock(), "tenant-1", account_id, app_model, "node-1" + WorkflowAgentComposerSaveToRosterApi(), + composer_save_payload, + MagicMock(), + "tenant-1", + account_id, + app_model, + "node-1", )["save_options"] == ["node_job_only"] @@ -1325,7 +1434,17 @@ def test_workflow_composer_copy_from_roster(app: Flask, monkeypatch: pytest.Monk } ): result = unwrap(WorkflowAgentComposerCopyFromRosterApi.post)( - WorkflowAgentComposerCopyFromRosterApi(), MagicMock(), "tenant-1", account_id, app_model, "node-1" + WorkflowAgentComposerCopyFromRosterApi(), + WorkflowComposerCopyFromRosterPayload( + source_agent_id="roster-agent-1", + source_snapshot_id="roster-version-1", + idempotency_key="copy-1", + ), + MagicMock(), + "tenant-1", + account_id, + app_model, + "node-1", ) assert result["binding"]["binding_type"] == "inline_agent" captured.pop("session", None) @@ -1344,7 +1463,15 @@ def test_workflow_impact_returns_empty_without_version(app: Flask) -> None: payload = {"variant": ComposerVariant.WORKFLOW.value, "save_strategy": ComposerSaveStrategy.NODE_JOB_ONLY.value} with app.test_request_context(json=payload): result = unwrap(WorkflowAgentComposerImpactApi.post)( - WorkflowAgentComposerImpactApi(), MagicMock(), "tenant-1", SimpleNamespace(id="app-1"), "node-1" + WorkflowAgentComposerImpactApi(), + ComposerSavePayload( + variant=ComposerVariant.WORKFLOW, + save_strategy=ComposerSaveStrategy.NODE_JOB_ONLY, + ), + MagicMock(), + "tenant-1", + SimpleNamespace(id="app-1"), + "node-1", ) assert result == {"current_snapshot_id": None, "workflow_node_count": 0, "bindings": []} @@ -1387,12 +1514,21 @@ def test_agent_composer_routes_resolve_app_from_agent_id( assert composer["variant"] == "agent_app" assert composer["active_config_is_published"] is True assert cast(dict[str, object], captured["load"])["agent_id"] == agent_id + composer_save_payload = ComposerSavePayload( + variant=ComposerVariant.AGENT_APP, + save_strategy=ComposerSaveStrategy.SAVE_TO_CURRENT_VERSION, + agent_soul={"prompt": {"system_prompt": "x"}}, + ) with app.test_request_context(json=payload): - saved_composer = unwrap(AgentComposerApi.put)(AgentComposerApi(), MagicMock(), "tenant-1", account_id, agent_id) + saved_composer = unwrap(AgentComposerApi.put)( + AgentComposerApi(), composer_save_payload, MagicMock(), "tenant-1", account_id, agent_id + ) assert saved_composer["variant"] == "agent_app" assert saved_composer["active_config_is_published"] is True assert cast(dict[str, object], captured["save"])["agent_id"] == agent_id - assert unwrap(AgentComposerValidateApi.post)(AgentComposerValidateApi(), MagicMock(), "tenant-1", agent_id) == { + assert unwrap(AgentComposerValidateApi.post)( + AgentComposerValidateApi(), composer_save_payload, MagicMock(), "tenant-1", agent_id + ) == { "result": "success", "errors": [], "warnings": [],