mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 12:59:18 +08:00
Merge 01528f7392 into 271019006e
This commit is contained in:
commit
4aecb84535
@ -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
|
||||
|
||||
51
api/core/workflow/nodes/agent/clarification_helper.py
Normal file
51
api/core/workflow/nodes/agent/clarification_helper.py
Normal 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
|
||||
@ -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]
|
||||
|
||||
@ -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
|
||||
176
web/app/components/workflow/nodes/agent/panel.spec.tsx
Normal file
176
web/app/components/workflow/nodes/agent/panel.spec.tsx
Normal 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,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -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>
|
||||
|
||||
@ -13,6 +13,7 @@ export type AgentNodeType = CommonNodeType & {
|
||||
memory?: Memory
|
||||
version?: string
|
||||
tool_node_version?: string
|
||||
enable_human_clarification?: boolean
|
||||
}
|
||||
|
||||
export enum AgentFeature {
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user