diff --git a/api/controllers/console/app/workflow.py b/api/controllers/console/app/workflow.py index dcd24d2200..3bf9f2da74 100644 --- a/api/controllers/console/app/workflow.py +++ b/api/controllers/console/app/workflow.py @@ -221,7 +221,7 @@ class DraftWorkflowApi(Resource): @setup_required @login_required @account_initialization_required - @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) + @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW, AppMode.AGENT]) @marshal_with(workflow_model) @edit_permission_required def get(self, app_model: App): @@ -241,7 +241,7 @@ class DraftWorkflowApi(Resource): @setup_required @login_required @account_initialization_required - @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) + @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW, AppMode.AGENT]) @console_ns.doc("sync_draft_workflow") @console_ns.doc(description="Sync draft workflow configuration") @console_ns.expect(console_ns.models[SyncDraftWorkflowPayload.__name__]) @@ -733,7 +733,7 @@ class WorkflowTaskStopApi(Resource): @setup_required @login_required @account_initialization_required - @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) + @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW, AppMode.AGENT]) @edit_permission_required def post(self, app_model: App, task_id: str): """ @@ -761,7 +761,7 @@ class DraftWorkflowNodeRunApi(Resource): @setup_required @login_required @account_initialization_required - @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) + @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW, AppMode.AGENT]) @marshal_with(workflow_run_node_execution_model) @edit_permission_required def post(self, app_model: App, node_id: str): @@ -807,7 +807,7 @@ class PublishedWorkflowApi(Resource): @setup_required @login_required @account_initialization_required - @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) + @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW, AppMode.AGENT]) @marshal_with(workflow_model) @edit_permission_required def get(self, app_model: App): @@ -825,7 +825,7 @@ class PublishedWorkflowApi(Resource): @setup_required @login_required @account_initialization_required - @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) + @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW, AppMode.AGENT]) @edit_permission_required def post(self, app_model: App): """ @@ -869,7 +869,7 @@ class DefaultBlockConfigsApi(Resource): @setup_required @login_required @account_initialization_required - @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) + @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW, AppMode.AGENT]) @edit_permission_required def get(self, app_model: App): """ @@ -891,7 +891,7 @@ class DefaultBlockConfigApi(Resource): @setup_required @login_required @account_initialization_required - @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) + @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW, AppMode.AGENT]) @edit_permission_required def get(self, app_model: App, block_type: str): """ @@ -956,7 +956,7 @@ class PublishedAllWorkflowApi(Resource): @setup_required @login_required @account_initialization_required - @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) + @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW, AppMode.AGENT]) @marshal_with(workflow_pagination_model) @edit_permission_required def get(self, app_model: App): @@ -1005,7 +1005,7 @@ class DraftWorkflowRestoreApi(Resource): @setup_required @login_required @account_initialization_required - @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) + @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW, AppMode.AGENT]) @edit_permission_required def post(self, app_model: App, workflow_id: str): current_user, _ = current_account_with_tenant() @@ -1043,7 +1043,7 @@ class WorkflowByIdApi(Resource): @setup_required @login_required @account_initialization_required - @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) + @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW, AppMode.AGENT]) @marshal_with(workflow_model) @edit_permission_required def patch(self, app_model: App, workflow_id: str): @@ -1083,7 +1083,7 @@ class WorkflowByIdApi(Resource): @setup_required @login_required @account_initialization_required - @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) + @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW, AppMode.AGENT]) @edit_permission_required def delete(self, app_model: App, workflow_id: str): """ @@ -1118,7 +1118,7 @@ class DraftWorkflowNodeLastRunApi(Resource): @setup_required @login_required @account_initialization_required - @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) + @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW, AppMode.AGENT]) @marshal_with(workflow_run_node_execution_model) def get(self, app_model: App, node_id: str): srv = WorkflowService() diff --git a/api/controllers/console/app/workflow_draft_variable.py b/api/controllers/console/app/workflow_draft_variable.py index f6d076320c..dc73229c6f 100644 --- a/api/controllers/console/app/workflow_draft_variable.py +++ b/api/controllers/console/app/workflow_draft_variable.py @@ -208,7 +208,7 @@ def _api_prerequisite[**P, R](f: Callable[P, R]) -> Callable[P, R | Response]: @login_required @account_initialization_required @edit_permission_required - @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) + @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW, AppMode.AGENT]) @wraps(f) def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | Response: return f(*args, **kwargs) diff --git a/api/controllers/console/app/workflow_run.py b/api/controllers/console/app/workflow_run.py index 83e8bedc11..0063277d0e 100644 --- a/api/controllers/console/app/workflow_run.py +++ b/api/controllers/console/app/workflow_run.py @@ -349,7 +349,7 @@ class WorkflowRunListApi(Resource): @setup_required @login_required @account_initialization_required - @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) + @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW, AppMode.AGENT]) @marshal_with(workflow_run_pagination_model) def get(self, app_model: App): """ @@ -397,7 +397,7 @@ class WorkflowRunCountApi(Resource): @setup_required @login_required @account_initialization_required - @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) + @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW, AppMode.AGENT]) @marshal_with(workflow_run_count_model) def get(self, app_model: App): """ @@ -434,7 +434,7 @@ class WorkflowRunDetailApi(Resource): @setup_required @login_required @account_initialization_required - @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) + @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW, AppMode.AGENT]) @marshal_with(workflow_run_detail_model) def get(self, app_model: App, run_id): """ @@ -458,7 +458,7 @@ class WorkflowRunNodeExecutionListApi(Resource): @setup_required @login_required @account_initialization_required - @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) + @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW, AppMode.AGENT]) @marshal_with(workflow_run_node_execution_list_model) def get(self, app_model: App, run_id): """ diff --git a/api/controllers/service_api/app/completion.py b/api/controllers/service_api/app/completion.py index 3142e5118e..1d3f3fb267 100644 --- a/api/controllers/service_api/app/completion.py +++ b/api/controllers/service_api/app/completion.py @@ -194,7 +194,7 @@ class ChatApi(Resource): Supports conversation management and both blocking and streaming response modes. """ app_mode = AppMode.value_of(app_model.mode) - if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: + if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT}: raise NotChatAppError() payload = ChatRequestPayload.model_validate(service_api_ns.payload or {}) @@ -258,7 +258,7 @@ class ChatStopApi(Resource): def post(self, app_model: App, end_user: EndUser, task_id: str): """Stop a running chat message generation.""" app_mode = AppMode.value_of(app_model.mode) - if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: + if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT}: raise NotChatAppError() AppTaskService.stop_task( diff --git a/api/controllers/service_api/app/conversation.py b/api/controllers/service_api/app/conversation.py index 8c9a3eb5e9..75f471d1c8 100644 --- a/api/controllers/service_api/app/conversation.py +++ b/api/controllers/service_api/app/conversation.py @@ -109,7 +109,7 @@ class ConversationApi(Resource): Supports pagination using last_id and limit parameters. """ app_mode = AppMode.value_of(app_model.mode) - if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: + if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT}: raise NotChatAppError() query_args = ConversationListQuery.model_validate(request.args.to_dict()) @@ -153,7 +153,7 @@ class ConversationDetailApi(Resource): def delete(self, app_model: App, end_user: EndUser, c_id): """Delete a specific conversation.""" app_mode = AppMode.value_of(app_model.mode) - if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: + if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT}: raise NotChatAppError() conversation_id = str(c_id) @@ -182,7 +182,7 @@ class ConversationRenameApi(Resource): def post(self, app_model: App, end_user: EndUser, c_id): """Rename a conversation or auto-generate a name.""" app_mode = AppMode.value_of(app_model.mode) - if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: + if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT}: raise NotChatAppError() conversation_id = str(c_id) @@ -224,7 +224,7 @@ class ConversationVariablesApi(Resource): """ # conversational variable only for chat app app_mode = AppMode.value_of(app_model.mode) - if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: + if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT}: raise NotChatAppError() conversation_id = str(c_id) @@ -263,7 +263,7 @@ class ConversationVariableDetailApi(Resource): The value must match the variable's expected type. """ app_mode = AppMode.value_of(app_model.mode) - if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: + if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT}: raise NotChatAppError() conversation_id = str(c_id) diff --git a/api/controllers/service_api/app/message.py b/api/controllers/service_api/app/message.py index 77fee9c142..676048ba84 100644 --- a/api/controllers/service_api/app/message.py +++ b/api/controllers/service_api/app/message.py @@ -65,7 +65,7 @@ class MessageListApi(Resource): Retrieves messages with pagination support using first_id. """ app_mode = AppMode.value_of(app_model.mode) - if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: + if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT}: raise NotChatAppError() query_args = MessageListQuery.model_validate(request.args.to_dict()) @@ -170,7 +170,7 @@ class MessageSuggestedApi(Resource): """ message_id = str(message_id) app_mode = AppMode.value_of(app_model.mode) - if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: + if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT}: raise NotChatAppError() try: diff --git a/api/core/workflow/nodes/agent_v2/node.py b/api/core/workflow/nodes/agent_v2/node.py index 02714129ad..397808e6ab 100644 --- a/api/core/workflow/nodes/agent_v2/node.py +++ b/api/core/workflow/nodes/agent_v2/node.py @@ -87,6 +87,37 @@ class AgentV2Node(Node[AgentV2NodeData]): def version(cls) -> str: return "1" + @classmethod + def get_default_config(cls, filters: Mapping[str, object] | None = None) -> Mapping[str, object]: + return { + "type": AGENT_V2_NODE_TYPE, + "config": { + "prompt_templates": { + "chat_model": { + "prompts": [ + { + "role": "system", + "text": "You are a helpful AI assistant.", + "edition_type": "basic", + } + ] + }, + "completion_model": { + "conversation_histories_role": { + "user_prefix": "Human", + "assistant_prefix": "Assistant", + }, + "prompt": { + "text": "{{#sys.query#}}", + "edition_type": "basic", + }, + }, + }, + "agent_strategy": "auto", + "max_iterations": 10, + }, + } + def _run(self) -> Generator[NodeEventBase, None, None]: dify_ctx = DifyRunContext.model_validate(self.require_run_context_value(DIFY_RUN_CONTEXT_KEY)) diff --git a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_agent_v2_phase7.py b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_agent_v2_phase7.py new file mode 100644 index 0000000000..723d2a3a12 --- /dev/null +++ b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_agent_v2_phase7.py @@ -0,0 +1,115 @@ +"""Tests for Phase 7 — New/old agent node parallel compatibility.""" + +import pytest + + +class TestAgentV2DefaultConfig: + """Verify Agent V2 node provides default block configuration.""" + + def test_has_default_config(self): + from core.workflow.node_factory import register_nodes + + register_nodes() + + from graphon.nodes.base.node import Node + + registry = Node.get_node_type_classes_mapping() + agent_v2_cls = registry["agent-v2"]["latest"] + config = agent_v2_cls.get_default_config() + + assert config, "Agent V2 should have a default config" + assert config["type"] == "agent-v2" + assert "config" in config + assert "prompt_templates" in config["config"] + assert "agent_strategy" in config["config"] + assert config["config"]["agent_strategy"] == "auto" + assert config["config"]["max_iterations"] == 10 + + def test_old_agent_no_default_config(self): + from core.workflow.node_factory import register_nodes + + register_nodes() + + from graphon.nodes.base.node import Node + + registry = Node.get_node_type_classes_mapping() + agent_cls = registry["agent"]["latest"] + config = agent_cls.get_default_config() + assert config == {} or config is None or not config + + +class TestParallelNodeRegistration: + """Verify both agent and agent-v2 coexist in the registry.""" + + def test_both_registered(self): + from core.workflow.node_factory import register_nodes + + register_nodes() + + from graphon.nodes.base.node import Node + + registry = Node.get_node_type_classes_mapping() + assert "agent" in registry + assert "agent-v2" in registry + + def test_different_classes(self): + from core.workflow.node_factory import register_nodes + + register_nodes() + + from graphon.nodes.base.node import Node + + registry = Node.get_node_type_classes_mapping() + old_cls = registry["agent"]["latest"] + new_cls = registry["agent-v2"]["latest"] + assert old_cls is not new_cls + + def test_default_configs_list_contains_agent_v2(self): + """Verify agent-v2 appears in the full default block configs list. + + Instead of instantiating WorkflowService (which requires Flask/DB), + we replicate the same iteration logic over the node registry. + """ + from core.workflow.node_factory import LATEST_VERSION, get_node_type_classes_mapping, register_nodes + + register_nodes() + + types_with_config: set[str] = set() + for node_type, mapping in get_node_type_classes_mapping().items(): + node_cls = mapping.get(LATEST_VERSION) + if node_cls: + cfg = node_cls.get_default_config() + if cfg and isinstance(cfg, dict): + types_with_config.add(cfg.get("type", "")) + + assert "agent-v2" in types_with_config + + +class TestAgentModeWorkflowAccess: + """Verify AGENT mode is allowed in workflow-related API mode checks.""" + + def test_workflow_controller_allows_agent(self): + """Check that the workflow.py source allows AppMode.AGENT.""" + import inspect + + from controllers.console.app import workflow + + source = inspect.getsource(workflow) + assert "AppMode.AGENT" in source + + def test_service_api_chat_allows_agent(self): + """Check that service API chat endpoint allows AGENT mode.""" + import inspect + + from controllers.service_api.app import completion + + source = inspect.getsource(completion) + assert "AppMode.AGENT" in source + + def test_service_api_conversation_allows_agent(self): + import inspect + + from controllers.service_api.app import conversation + + source = inspect.getsource(conversation) + assert "AppMode.AGENT" in source