This commit is contained in:
Haohao 2026-05-09 09:42:02 +08:00 committed by GitHub
commit 4aecb84535
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 335 additions and 0 deletions

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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

View File

@ -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<string, unknown>) => 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: () => <div />,
}))
vi.mock('../_base/components/mcp-tool-availability', () => ({
MCPToolAvailabilityProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}))
vi.mock('../_base/components/memory-config', () => ({
default: () => <div />,
}))
vi.mock('../_base/components/output-vars', () => ({
default: ({ children }: { children: React.ReactNode }) => <>{children}</>,
VarItem: () => <div />,
}))
vi.mock('../_base/components/split', () => ({
default: () => <div />,
}))
// Mock FormInputBoolean to capture onChange callback
vi.mock('../_base/components/form-input-boolean', () => ({
default: ({ onChange }: { value: boolean, onChange: (value: boolean) => void }) => {
capturedOnChange = onChange
return <div data-testid="form-input-boolean" />
},
}))
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(
<AgentPanel
id="test-node"
data={mockData}
panelProps={mockPanelProps}
/>,
)
// 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(
<AgentPanel
id="test-node"
data={mockData}
panelProps={mockPanelProps}
/>,
)
// 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(
<AgentPanel
id="test-node"
data={mockData}
panelProps={mockPanelProps}
/>,
)
// 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,
}),
)
})
})

View File

@ -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<NodePanelProps<AgentNodeType>> = (props) => {
/>
</>
)}
<Split />
<Field
title={t('nodes.agent.enableHumanClarification.label', { ns: 'workflow' })}
tooltip={t('nodes.agent.enableHumanClarification.tooltip', { ns: 'workflow' })}
className="px-0 py-2"
>
<FormInputBoolean
value={inputs.enable_human_clarification ?? false}
onChange={(value) => {
setInputs({
...inputs,
enable_human_clarification: value,
})
}}
/>
</Field>
</div>
<div>
<OutputVars>

View File

@ -13,6 +13,7 @@ export type AgentNodeType = CommonNodeType & {
memory?: Memory
version?: string
tool_node_version?: string
enable_human_clarification?: boolean
}
export enum AgentFeature {

View File

@ -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",