diff --git a/api/core/workflow/nodes/agent/agent_node.py b/api/core/workflow/nodes/agent/agent_node.py index 68a24e86b1..d8b0ea3afd 100644 --- a/api/core/workflow/nodes/agent/agent_node.py +++ b/api/core/workflow/nodes/agent/agent_node.py @@ -10,6 +10,7 @@ from graphon.node_events import NodeEventBase, NodeRunResult, StreamCompletedEve from graphon.nodes.base.node import Node from graphon.nodes.base.variable_template_parser import VariableTemplateParser +from .clarification_helper import should_enable_clarification from .entities import AgentNodeData from .exceptions import ( AgentInvocationError, @@ -154,6 +155,11 @@ class AgentNode(Node[AgentNodeData]): node_id=self._node_id, node_execution_id=self.id, ) + # Extensibility hook for human clarification (HITL support) + # Currently a no-op, but allows future HITL implementation + if should_enable_clarification(self.node_data): + # Placeholder for future clarification logic + pass except PluginDaemonClientSideError as e: transform_error = AgentMessageTransformError( f"Failed to transform agent message: {str(e)}", original_error=e diff --git a/api/core/workflow/nodes/agent/clarification_helper.py b/api/core/workflow/nodes/agent/clarification_helper.py new file mode 100644 index 0000000000..0debf19f47 --- /dev/null +++ b/api/core/workflow/nodes/agent/clarification_helper.py @@ -0,0 +1,51 @@ +""" +Clarification helper for agent node human-in-the-loop support. + +This module provides extensibility hooks for future HITL (Human-In-The-Loop) features. +Currently, it serves as a placeholder for clarification request extraction and handling. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from .entities import AgentNodeData + + +def should_enable_clarification(node_data: AgentNodeData) -> bool: + """ + Check if human clarification is enabled for this agent node. + + Args: + node_data: The agent node data configuration. + + Returns: + True if human clarification is enabled, False otherwise. + """ + return node_data.enable_human_clarification + + +def extract_clarification_request( + _agent_output: dict[str, Any], + enable_clarification: bool, +) -> dict[str, Any] | None: + """ + Extract clarification request from agent output if enabled. + + This is a placeholder for future HITL implementation. + Currently returns None as clarification is not yet implemented. + + Args: + _agent_output: The output from agent execution. Currently unused, reserved for future HITL expansion. + enable_clarification: Whether clarification is enabled. + + Returns: + Clarification request dict if found and enabled, None otherwise. + """ + if not enable_clarification: + return None + + # Placeholder for future clarification extraction logic + # This will be extended when HITL feature is fully implemented + return None diff --git a/api/core/workflow/nodes/agent/entities.py b/api/core/workflow/nodes/agent/entities.py index 51452c29a3..59be05f121 100644 --- a/api/core/workflow/nodes/agent/entities.py +++ b/api/core/workflow/nodes/agent/entities.py @@ -19,6 +19,8 @@ class AgentNodeData(BaseNodeData): # If this value is None, it indicates this is a previous version # and requires using the legacy parameter parsing rules. tool_node_version: str | None = None + # Enable human clarification for agent reasoning + enable_human_clarification: bool = False class AgentInput(BaseModel): value: Union[list[str], list[ToolSelector], Any] diff --git a/api/tests/unit_tests/core/workflow/nodes/agent/test_clarification_helper.py b/api/tests/unit_tests/core/workflow/nodes/agent/test_clarification_helper.py new file mode 100644 index 0000000000..d15e173ad8 --- /dev/null +++ b/api/tests/unit_tests/core/workflow/nodes/agent/test_clarification_helper.py @@ -0,0 +1,80 @@ +""" +Tests for agent node clarification helper and configuration. +""" + +import pytest + +from core.workflow.nodes.agent.clarification_helper import ( + extract_clarification_request, + should_enable_clarification, +) +from core.workflow.nodes.agent.entities import AgentNodeData + + +@pytest.fixture +def base_node_data_args() -> dict: + """Fixture providing base arguments for AgentNodeData construction.""" + return { + "id": "test_node", + "type": "agent", + "agent_strategy_provider_name": "test_provider", + "agent_strategy_name": "test_strategy", + "agent_strategy_label": "Test Strategy", + "agent_parameters": {}, + } + + +class TestClarificationHelper: + """Test suite for clarification helper functions.""" + + def test_should_enable_clarification_when_enabled(self, base_node_data_args): + """Test that should_enable_clarification returns True when enabled.""" + node_data = AgentNodeData( + **base_node_data_args, + enable_human_clarification=True, + ) + assert should_enable_clarification(node_data) is True + + def test_should_enable_clarification_when_disabled(self, base_node_data_args): + """Test that should_enable_clarification returns False when disabled.""" + node_data = AgentNodeData( + **base_node_data_args, + enable_human_clarification=False, + ) + assert should_enable_clarification(node_data) is False + + def test_should_enable_clarification_default_false(self, base_node_data_args): + """Test that clarification is disabled by default.""" + node_data = AgentNodeData(**base_node_data_args) + assert should_enable_clarification(node_data) is False + + def test_extract_clarification_request_when_disabled(self): + """Test that extract_clarification_request returns None when disabled.""" + result = extract_clarification_request( + _agent_output={"text": "test output"}, + enable_clarification=False, + ) + assert result is None + + def test_extract_clarification_request_when_enabled(self): + """Test that extract_clarification_request returns None when enabled (placeholder).""" + # Currently returns None as placeholder for future implementation + result = extract_clarification_request( + _agent_output={"text": "test output"}, + enable_clarification=True, + ) + assert result is None + + def test_agent_node_data_with_clarification_field(self, base_node_data_args): + """Test that AgentNodeData properly stores enable_human_clarification.""" + node_data = AgentNodeData( + **base_node_data_args, + enable_human_clarification=True, + ) + assert node_data.enable_human_clarification is True + + node_data_disabled = AgentNodeData( + **base_node_data_args, + enable_human_clarification=False, + ) + assert node_data_disabled.enable_human_clarification is False diff --git a/web/app/components/workflow/nodes/agent/panel.spec.tsx b/web/app/components/workflow/nodes/agent/panel.spec.tsx new file mode 100644 index 0000000000..1884a4dc0e --- /dev/null +++ b/web/app/components/workflow/nodes/agent/panel.spec.tsx @@ -0,0 +1,176 @@ +/** + * Agent Panel - Enable Human Clarification Test + * + * Tests the enable_human_clarification onChange handler in the agent panel. + * Covers panel.tsx line 114: setInputs({ ...inputs, enable_human_clarification: value }) + * + * Strategy: Mock FormInputBoolean to capture and trigger its onChange callback directly, + * avoiding dependency on component's internal text/UI. + */ + +import type { AgentNodeType } from './types' +import { render } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import AgentPanel from './panel' + +// Mock setInputs at the top level +const mockSetInputs = vi.fn() + +// Stub for panelProps - using any to avoid complex type requirements in test +// eslint-disable-next-line ts/no-explicit-any +const mockPanelProps = {} as any + +// Capture FormInputBoolean's onChange callback +let capturedOnChange: ((value: boolean) => void) | null = null + +// Mock useConfig to return minimal required data +vi.mock('./use-config', () => ({ + default: () => ({ + inputs: { + enable_human_clarification: false, + agent_strategy_name: 'test-strategy', + agent_strategy_provider_name: 'test-provider', + agent_strategy_label: 'Test Strategy', + meta: { version: '1.0' }, + agent_parameters: {}, + output_schema: {}, + memory: undefined, + }, + setInputs: mockSetInputs, + currentStrategy: null, + formData: {}, + onFormChange: vi.fn(), + isChatMode: false, + availableNodesWithParent: [], + availableVars: [], + readOnly: false, + outputSchema: [], + handleMemoryChange: vi.fn(), + }), +})) + +// Mock i18n +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +// Mock store +vi.mock('../../store', () => ({ + useStore: (selector: (state: Record) => unknown) => { + const state = { + setControlPromptEditorRerenderKey: vi.fn(), + } + return selector(state) + }, +})) + +// Mock utility +vi.mock('@/utils/plugin-version-feature', () => ({ + isSupportMCP: () => true, +})) + +// Mock heavy components +vi.mock('../_base/components/agent-strategy', () => ({ + AgentStrategy: () =>
, +})) + +vi.mock('../_base/components/mcp-tool-availability', () => ({ + MCPToolAvailabilityProvider: ({ children }: { children: React.ReactNode }) => <>{children}, +})) + +vi.mock('../_base/components/memory-config', () => ({ + default: () =>
, +})) + +vi.mock('../_base/components/output-vars', () => ({ + default: ({ children }: { children: React.ReactNode }) => <>{children}, + VarItem: () =>
, +})) + +vi.mock('../_base/components/split', () => ({ + default: () =>
, +})) + +// Mock FormInputBoolean to capture onChange callback +vi.mock('../_base/components/form-input-boolean', () => ({ + default: ({ onChange }: { value: boolean, onChange: (value: boolean) => void }) => { + capturedOnChange = onChange + return
+ }, +})) + +describe('AgentPanel - Enable Human Clarification', () => { + beforeEach(() => { + mockSetInputs.mockClear() + capturedOnChange = null + }) + + it('should call setInputs with enable_human_clarification=true when onChange is triggered with true', () => { + const mockData = {} as AgentNodeType + + render( + , + ) + + // Trigger the captured onChange callback + capturedOnChange?.(true) + + expect(mockSetInputs).toHaveBeenCalledWith( + expect.objectContaining({ + enable_human_clarification: true, + }), + ) + }) + + it('should call setInputs with enable_human_clarification=false when onChange is triggered with false', () => { + const mockData = {} as AgentNodeType + + render( + , + ) + + // Trigger the captured onChange callback + capturedOnChange?.(false) + + expect(mockSetInputs).toHaveBeenCalledWith( + expect.objectContaining({ + enable_human_clarification: false, + }), + ) + }) + + it('should preserve other inputs when updating enable_human_clarification', () => { + const mockData = {} as AgentNodeType + + render( + , + ) + + // Trigger the captured onChange callback + capturedOnChange?.(true) + + // Verify that spread operator preserves other inputs + expect(mockSetInputs).toHaveBeenCalledWith( + expect.objectContaining({ + agent_strategy_name: 'test-strategy', + agent_strategy_provider_name: 'test-provider', + agent_strategy_label: 'Test Strategy', + enable_human_clarification: true, + }), + ) + }) +}) diff --git a/web/app/components/workflow/nodes/agent/panel.tsx b/web/app/components/workflow/nodes/agent/panel.tsx index 03e3aafa49..cd98d8604a 100644 --- a/web/app/components/workflow/nodes/agent/panel.tsx +++ b/web/app/components/workflow/nodes/agent/panel.tsx @@ -10,6 +10,7 @@ import { isSupportMCP } from '@/utils/plugin-version-feature' import { useStore } from '../../store' import { AgentStrategy } from '../_base/components/agent-strategy' import Field from '../_base/components/field' +import FormInputBoolean from '../_base/components/form-input-boolean' import { MCPToolAvailabilityProvider } from '../_base/components/mcp-tool-availability' import MemoryConfig from '../_base/components/memory-config' import OutputVars, { VarItem } from '../_base/components/output-vars' @@ -101,6 +102,22 @@ const AgentPanel: FC> = (props) => { /> )} + + + { + setInputs({ + ...inputs, + enable_human_clarification: value, + }) + }} + /> +
diff --git a/web/app/components/workflow/nodes/agent/types.ts b/web/app/components/workflow/nodes/agent/types.ts index efc7c0cd9a..280beb9507 100644 --- a/web/app/components/workflow/nodes/agent/types.ts +++ b/web/app/components/workflow/nodes/agent/types.ts @@ -13,6 +13,7 @@ export type AgentNodeType = CommonNodeType & { memory?: Memory version?: string tool_node_version?: string + enable_human_clarification?: boolean } export enum AgentFeature { diff --git a/web/i18n/en-US/workflow.json b/web/i18n/en-US/workflow.json index 3bb285d501..bfcb9ab053 100644 --- a/web/i18n/en-US/workflow.json +++ b/web/i18n/en-US/workflow.json @@ -369,6 +369,8 @@ "nodes.agent.checkList.strategyNotSelected": "Strategy not selected", "nodes.agent.clickToViewParameterSchema": "Click to view parameter schema", "nodes.agent.configureModel": "Configure Model", + "nodes.agent.enableHumanClarification.label": "Enable Human Clarification", + "nodes.agent.enableHumanClarification.tooltip": "Allow human intervention to clarify agent reasoning during execution", "nodes.agent.installPlugin.cancel": "Cancel", "nodes.agent.installPlugin.changelog": "Change log", "nodes.agent.installPlugin.desc": "About to install the following plugin",