This commit is contained in:
yujiosaka 2025-12-29 15:26:00 +09:00 committed by GitHub
commit e1b27e2cf9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 624 additions and 9 deletions

View File

@ -9,10 +9,11 @@ from sqlalchemy.orm import Session
from core.agent.entities import AgentToolEntity from core.agent.entities import AgentToolEntity
from core.agent.plugin_entities import AgentStrategyParameter from core.agent.plugin_entities import AgentStrategyParameter
from core.file import File, FileTransferMethod from core.file import File, FileTransferMethod, FileType, file_manager
from core.memory.token_buffer_memory import TokenBufferMemory from core.memory.token_buffer_memory import TokenBufferMemory
from core.model_manager import ModelInstance, ModelManager from core.model_manager import ModelInstance, ModelManager
from core.model_runtime.entities.llm_entities import LLMUsage, LLMUsageMetadata from core.model_runtime.entities.llm_entities import LLMUsage, LLMUsageMetadata
from core.model_runtime.entities.message_entities import TextPromptMessageContent
from core.model_runtime.entities.model_entities import AIModelEntity, ModelType from core.model_runtime.entities.model_entities import AIModelEntity, ModelType
from core.model_runtime.utils.encoders import jsonable_encoder from core.model_runtime.utils.encoders import jsonable_encoder
from core.provider_manager import ProviderManager from core.provider_manager import ProviderManager
@ -24,7 +25,7 @@ from core.tools.entities.tool_entities import (
) )
from core.tools.tool_manager import ToolManager from core.tools.tool_manager import ToolManager
from core.tools.utils.message_transformer import ToolFileMessageTransformer from core.tools.utils.message_transformer import ToolFileMessageTransformer
from core.variables.segments import ArrayFileSegment, StringSegment from core.variables.segments import ArrayFileSegment, FileSegment, StringSegment
from core.workflow.enums import ( from core.workflow.enums import (
NodeType, NodeType,
SystemVariableKey, SystemVariableKey,
@ -160,6 +161,22 @@ class AgentNode(Node[AgentNodeData]):
) )
) )
def _fetch_files_from_variable_selector(
self,
*,
variable_pool: VariablePool,
selector: Sequence[str],
) -> Sequence[File]:
"""Fetch files from a variable selector."""
variable = variable_pool.get(list(selector))
if variable is None:
return []
elif isinstance(variable, FileSegment):
return [variable.value]
elif isinstance(variable, ArrayFileSegment):
return variable.value
return []
def _generate_agent_parameters( def _generate_agent_parameters(
self, self,
*, *,
@ -206,11 +223,52 @@ class AgentNode(Node[AgentNodeData]):
except TypeError: except TypeError:
parameter_value = str(agent_input.value) parameter_value = str(agent_input.value)
segment_group = variable_pool.convert_template(parameter_value) segment_group = variable_pool.convert_template(parameter_value)
if parameter_name in ("query", "instruction") and not for_log:
contents: list[dict[str, Any]] = []
has_file = False
vision_detail = node_data.vision.configs.detail if node_data.vision.enabled else None
for segment in segment_group.value:
if isinstance(segment, (ArrayFileSegment, FileSegment)):
files = segment.value if isinstance(segment, ArrayFileSegment) else [segment.value]
for file in files:
if file.type in {FileType.IMAGE, FileType.VIDEO, FileType.AUDIO, FileType.DOCUMENT}:
file_content = file_manager.to_prompt_message_content(
file, image_detail_config=vision_detail
)
contents.append(file_content.model_dump())
has_file = True
else:
text = segment.text
if text:
contents.append(TextPromptMessageContent(data=text).model_dump())
if parameter_name == "query":
if node_data.vision.enabled and node_data.vision.configs.variable_selector:
vision_files = self._fetch_files_from_variable_selector(
variable_pool=variable_pool,
selector=node_data.vision.configs.variable_selector,
)
for file in vision_files:
if file.type in {FileType.IMAGE, FileType.VIDEO, FileType.AUDIO, FileType.DOCUMENT}:
file_content = file_manager.to_prompt_message_content(
file, image_detail_config=vision_detail
)
contents.append(file_content.model_dump())
has_file = True
if has_file:
parameter_value = contents
else:
parameter_value = segment_group.text
else:
parameter_value = segment_group.log if for_log else segment_group.text parameter_value = segment_group.log if for_log else segment_group.text
# variable_pool.convert_template returns a string, # variable_pool.convert_template returns a string,
# so we need to convert it back to a dictionary # so we need to convert it back to a dictionary
try: try:
if not isinstance(agent_input.value, str): if not isinstance(agent_input.value, str) and isinstance(parameter_value, str):
parameter_value = json.loads(parameter_value) parameter_value = json.loads(parameter_value)
except json.JSONDecodeError: except json.JSONDecodeError:
parameter_value = parameter_value parameter_value = parameter_value

View File

@ -1,18 +1,38 @@
from collections.abc import Sequence
from enum import IntEnum, StrEnum, auto from enum import IntEnum, StrEnum, auto
from typing import Any, Literal, Union from typing import Any, Literal, Union
from pydantic import BaseModel from pydantic import BaseModel, Field, field_validator
from core.model_runtime.entities import ImagePromptMessageContent
from core.prompt.entities.advanced_prompt_entities import MemoryConfig from core.prompt.entities.advanced_prompt_entities import MemoryConfig
from core.tools.entities.tool_entities import ToolSelector from core.tools.entities.tool_entities import ToolSelector
from core.workflow.nodes.base.entities import BaseNodeData from core.workflow.nodes.base.entities import BaseNodeData
class VisionConfigOptions(BaseModel):
variable_selector: Sequence[str] = Field(default_factory=lambda: ["sys", "files"])
detail: ImagePromptMessageContent.DETAIL = ImagePromptMessageContent.DETAIL.HIGH
class VisionConfig(BaseModel):
enabled: bool = False
configs: VisionConfigOptions = Field(default_factory=VisionConfigOptions)
@field_validator("configs", mode="before")
@classmethod
def convert_none_configs(cls, v: Any):
if v is None:
return VisionConfigOptions()
return v
class AgentNodeData(BaseNodeData): class AgentNodeData(BaseNodeData):
agent_strategy_provider_name: str # redundancy agent_strategy_provider_name: str # redundancy
agent_strategy_name: str agent_strategy_name: str
agent_strategy_label: str # redundancy agent_strategy_label: str # redundancy
memory: MemoryConfig | None = None memory: MemoryConfig | None = None
vision: VisionConfig = Field(default_factory=VisionConfig)
# The version of the tool parameter. # The version of the tool parameter.
# If this value is None, it indicates this is a previous version # If this value is None, it indicates this is a previous version
# and requires using the legacy parameter parsing rules. # and requires using the legacy parameter parsing rules.

View File

@ -0,0 +1,467 @@
"""Unit tests for AgentNode file handling."""
import sys
import types
from typing import TYPE_CHECKING, Any
from unittest.mock import MagicMock, patch
import pytest
if TYPE_CHECKING:
from core.workflow.nodes.agent.agent_node import AgentNode
def _setup_stubs(monkeypatch):
"""Set up stubs for circular import issues."""
module_name = "core.ops.ops_trace_manager"
if module_name not in sys.modules:
ops_stub = types.ModuleType(module_name)
ops_stub.TraceQueueManager = object
ops_stub.TraceTask = object
monkeypatch.setitem(sys.modules, module_name, ops_stub)
@pytest.fixture
def agent_imports(monkeypatch) -> dict[str, Any]:
"""Set up stubs and return imported modules."""
_setup_stubs(monkeypatch)
from core.agent.plugin_entities import AgentStrategyParameter
from core.file import File, FileTransferMethod, FileType, file_manager
from core.model_runtime.entities.message_entities import ImagePromptMessageContent
from core.variables import FileSegment, StringSegment
from core.workflow.nodes.agent.agent_node import AgentNode
from core.workflow.nodes.agent.entities import AgentNodeData, VisionConfig, VisionConfigOptions
from core.workflow.runtime.variable_pool import VariablePool
return {
"AgentNode": AgentNode,
"AgentNodeData": AgentNodeData,
"VisionConfig": VisionConfig,
"VisionConfigOptions": VisionConfigOptions,
"VariablePool": VariablePool,
"AgentStrategyParameter": AgentStrategyParameter,
"File": File,
"FileTransferMethod": FileTransferMethod,
"FileType": FileType,
"file_manager": file_manager,
"ImagePromptMessageContent": ImagePromptMessageContent,
"FileSegment": FileSegment,
"StringSegment": StringSegment,
}
class TestAgentNodeFileHandling:
"""Tests for file handling in query, instruction, and vision variable selector."""
@pytest.fixture
def mock_file(self, agent_imports):
"""Create a mock file."""
File = agent_imports["File"]
FileType = agent_imports["FileType"]
FileTransferMethod = agent_imports["FileTransferMethod"]
return File(
id="test-file-id",
tenant_id="test-tenant",
type=FileType.IMAGE,
transfer_method=FileTransferMethod.LOCAL_FILE,
related_id="test-related-id",
filename="test.png",
extension=".png",
mime_type="image/png",
size=1024,
)
@pytest.fixture
def mock_custom_file(self, agent_imports):
"""Create a mock custom (unsupported) file."""
File = agent_imports["File"]
FileType = agent_imports["FileType"]
FileTransferMethod = agent_imports["FileTransferMethod"]
return File(
id="test-custom-id",
tenant_id="test-tenant",
type=FileType.CUSTOM,
transfer_method=FileTransferMethod.LOCAL_FILE,
related_id="test-related-id",
filename="test.zip",
extension=".zip",
mime_type="application/zip",
size=4096,
)
@pytest.fixture
def mock_strategy(self, agent_imports) -> MagicMock:
"""Create a mock agent strategy."""
AgentStrategyParameter = agent_imports["AgentStrategyParameter"]
strategy = MagicMock()
strategy.get_parameters.return_value = [
AgentStrategyParameter(
name="query",
type=AgentStrategyParameter.AgentStrategyParameterType.STRING,
required=True,
label={"en_US": "Query"},
),
AgentStrategyParameter(
name="instruction",
type=AgentStrategyParameter.AgentStrategyParameterType.STRING,
required=False,
label={"en_US": "Instruction"},
),
]
return strategy
@pytest.fixture
def base_node_data(self, agent_imports) -> dict:
"""Create base node data for tests."""
VisionConfig = agent_imports["VisionConfig"]
return {
"title": "Test Agent",
"agent_strategy_provider_name": "test-provider",
"agent_strategy_name": "test-strategy",
"agent_strategy_label": "Test Strategy",
"agent_parameters": {},
"vision": VisionConfig(enabled=False),
}
def _create_agent_node(self, agent_imports) -> "AgentNode":
"""Create an AgentNode instance for testing."""
AgentNode = agent_imports["AgentNode"]
node = object.__new__(AgentNode)
node.tenant_id = "test-tenant"
node.app_id = "test-app"
return node
def test_query_with_text_only_returns_string(self, agent_imports, mock_strategy, base_node_data):
"""When query contains only text, it should return a string."""
# Arrange
VariablePool = agent_imports["VariablePool"]
AgentNodeData = agent_imports["AgentNodeData"]
StringSegment = agent_imports["StringSegment"]
variable_pool = VariablePool()
variable_pool.add(["node1", "text_var"], StringSegment(value="Hello, world!"))
base_node_data["agent_parameters"] = {
"query": AgentNodeData.AgentInput(
type="mixed",
value="{{#node1.text_var#}}",
),
}
node_data = AgentNodeData.model_validate(base_node_data)
agent_parameters = mock_strategy.get_parameters()
agent_node = self._create_agent_node(agent_imports)
# Act
result = agent_node._generate_agent_parameters(
agent_parameters=agent_parameters,
variable_pool=variable_pool,
node_data=node_data,
for_log=False,
strategy=mock_strategy,
)
# Assert
assert result["query"] == "Hello, world!"
assert isinstance(result["query"], str)
def test_query_with_file_returns_list(self, agent_imports, mock_file, mock_strategy, base_node_data):
"""When query contains a file, it should return a list."""
# Arrange
VariablePool = agent_imports["VariablePool"]
AgentNodeData = agent_imports["AgentNodeData"]
FileSegment = agent_imports["FileSegment"]
file_manager = agent_imports["file_manager"]
ImagePromptMessageContent = agent_imports["ImagePromptMessageContent"]
variable_pool = VariablePool()
variable_pool.add(["node1", "file_var"], FileSegment(value=mock_file))
base_node_data["agent_parameters"] = {
"query": AgentNodeData.AgentInput(
type="mixed",
value="{{#node1.file_var#}}",
),
}
node_data = AgentNodeData.model_validate(base_node_data)
agent_parameters = mock_strategy.get_parameters()
agent_node = self._create_agent_node(agent_imports)
with patch.object(file_manager, "to_prompt_message_content") as mock_to_content:
mock_to_content.return_value = ImagePromptMessageContent(
url="http://example.com/test.png", mime_type="image/png", format="png"
)
# Act
result = agent_node._generate_agent_parameters(
agent_parameters=agent_parameters,
variable_pool=variable_pool,
node_data=node_data,
for_log=False,
strategy=mock_strategy,
)
# Assert
assert isinstance(result["query"], list)
assert len(result["query"]) == 1
assert result["query"][0]["type"] == "image"
def test_query_with_text_and_file_returns_list_with_both(
self, agent_imports, mock_file, mock_strategy, base_node_data
):
"""When query contains both text and file, it should return a list with both."""
# Arrange
VariablePool = agent_imports["VariablePool"]
AgentNodeData = agent_imports["AgentNodeData"]
FileSegment = agent_imports["FileSegment"]
file_manager = agent_imports["file_manager"]
ImagePromptMessageContent = agent_imports["ImagePromptMessageContent"]
variable_pool = VariablePool()
variable_pool.add(["node1", "file_var"], FileSegment(value=mock_file))
base_node_data["agent_parameters"] = {
"query": AgentNodeData.AgentInput(
type="mixed",
value="Describe this: {{#node1.file_var#}}",
),
}
node_data = AgentNodeData.model_validate(base_node_data)
agent_parameters = mock_strategy.get_parameters()
agent_node = self._create_agent_node(agent_imports)
with patch.object(file_manager, "to_prompt_message_content") as mock_to_content:
mock_to_content.return_value = ImagePromptMessageContent(
url="http://example.com/test.png", mime_type="image/png", format="png"
)
# Act
result = agent_node._generate_agent_parameters(
agent_parameters=agent_parameters,
variable_pool=variable_pool,
node_data=node_data,
for_log=False,
strategy=mock_strategy,
)
# Assert
assert isinstance(result["query"], list)
assert len(result["query"]) == 2
assert result["query"][0]["type"] == "text"
assert result["query"][1]["type"] == "image"
def test_custom_file_type_is_ignored(self, agent_imports, mock_custom_file, mock_strategy, base_node_data):
"""Custom file types should be ignored and return text only."""
# Arrange
VariablePool = agent_imports["VariablePool"]
AgentNodeData = agent_imports["AgentNodeData"]
FileSegment = agent_imports["FileSegment"]
variable_pool = VariablePool()
variable_pool.add(["node1", "file_var"], FileSegment(value=mock_custom_file))
base_node_data["agent_parameters"] = {
"query": AgentNodeData.AgentInput(
type="mixed",
value="{{#node1.file_var#}}",
),
}
node_data = AgentNodeData.model_validate(base_node_data)
agent_parameters = mock_strategy.get_parameters()
agent_node = self._create_agent_node(agent_imports)
# Act
result = agent_node._generate_agent_parameters(
agent_parameters=agent_parameters,
variable_pool=variable_pool,
node_data=node_data,
for_log=False,
strategy=mock_strategy,
)
# Assert
# Custom file types are ignored, so result should be the text representation
assert isinstance(result["query"], str)
def test_instruction_with_file_returns_list(self, agent_imports, mock_file, mock_strategy, base_node_data):
"""When instruction contains a file, it should return a list (same as query)."""
# Arrange
VariablePool = agent_imports["VariablePool"]
AgentNodeData = agent_imports["AgentNodeData"]
FileSegment = agent_imports["FileSegment"]
file_manager = agent_imports["file_manager"]
ImagePromptMessageContent = agent_imports["ImagePromptMessageContent"]
variable_pool = VariablePool()
variable_pool.add(["node1", "file_var"], FileSegment(value=mock_file))
base_node_data["agent_parameters"] = {
"instruction": AgentNodeData.AgentInput(
type="mixed",
value="You are a helpful assistant. {{#node1.file_var#}}",
),
}
node_data = AgentNodeData.model_validate(base_node_data)
agent_parameters = mock_strategy.get_parameters()
agent_node = self._create_agent_node(agent_imports)
with patch.object(file_manager, "to_prompt_message_content") as mock_to_content:
mock_to_content.return_value = ImagePromptMessageContent(
url="http://example.com/test.png", mime_type="image/png", format="png"
)
# Act
result = agent_node._generate_agent_parameters(
agent_parameters=agent_parameters,
variable_pool=variable_pool,
node_data=node_data,
for_log=False,
strategy=mock_strategy,
)
# Assert
assert isinstance(result["instruction"], list)
assert len(result["instruction"]) == 2
assert result["instruction"][0]["type"] == "text"
assert result["instruction"][1]["type"] == "image"
def test_vision_variable_selector_files_added_to_query(
self, agent_imports, mock_file, mock_strategy, base_node_data
):
"""Vision variable selector files should be added to query only."""
# Arrange
VariablePool = agent_imports["VariablePool"]
AgentNodeData = agent_imports["AgentNodeData"]
StringSegment = agent_imports["StringSegment"]
FileSegment = agent_imports["FileSegment"]
VisionConfig = agent_imports["VisionConfig"]
VisionConfigOptions = agent_imports["VisionConfigOptions"]
file_manager = agent_imports["file_manager"]
ImagePromptMessageContent = agent_imports["ImagePromptMessageContent"]
variable_pool = VariablePool()
variable_pool.add(["node1", "text_var"], StringSegment(value="Describe this image"))
variable_pool.add(["sys", "files"], FileSegment(value=mock_file))
base_node_data["agent_parameters"] = {
"query": AgentNodeData.AgentInput(
type="mixed",
value="{{#node1.text_var#}}",
),
}
base_node_data["vision"] = VisionConfig(
enabled=True,
configs=VisionConfigOptions(
variable_selector=["sys", "files"],
),
)
node_data = AgentNodeData.model_validate(base_node_data)
agent_parameters = mock_strategy.get_parameters()
agent_node = self._create_agent_node(agent_imports)
with patch.object(file_manager, "to_prompt_message_content") as mock_to_content:
mock_to_content.return_value = ImagePromptMessageContent(
url="http://example.com/test.png", mime_type="image/png", format="png"
)
# Act
result = agent_node._generate_agent_parameters(
agent_parameters=agent_parameters,
variable_pool=variable_pool,
node_data=node_data,
for_log=False,
strategy=mock_strategy,
)
# Assert
assert isinstance(result["query"], list)
assert len(result["query"]) == 2
assert result["query"][0]["type"] == "text"
assert result["query"][0]["data"] == "Describe this image"
assert result["query"][1]["type"] == "image"
def test_for_log_returns_text_representation(self, agent_imports, mock_file, mock_strategy, base_node_data):
"""When for_log is True, files should be represented as log text."""
# Arrange
VariablePool = agent_imports["VariablePool"]
AgentNodeData = agent_imports["AgentNodeData"]
FileSegment = agent_imports["FileSegment"]
variable_pool = VariablePool()
variable_pool.add(["node1", "file_var"], FileSegment(value=mock_file))
base_node_data["agent_parameters"] = {
"query": AgentNodeData.AgentInput(
type="mixed",
value="{{#node1.file_var#}}",
),
}
node_data = AgentNodeData.model_validate(base_node_data)
agent_parameters = mock_strategy.get_parameters()
agent_node = self._create_agent_node(agent_imports)
# Act
result = agent_node._generate_agent_parameters(
agent_parameters=agent_parameters,
variable_pool=variable_pool,
node_data=node_data,
for_log=True,
strategy=mock_strategy,
)
# Assert
# for_log=True should return log representation, not a list
assert isinstance(result["query"], str)
def test_non_query_instruction_parameter_returns_text(
self, agent_imports, mock_file, mock_strategy, base_node_data
):
"""Parameters other than query/instruction should return text even with files."""
# Arrange
AgentStrategyParameter = agent_imports["AgentStrategyParameter"]
VariablePool = agent_imports["VariablePool"]
AgentNodeData = agent_imports["AgentNodeData"]
FileSegment = agent_imports["FileSegment"]
mock_strategy.get_parameters.return_value = [
AgentStrategyParameter(
name="other_param",
type=AgentStrategyParameter.AgentStrategyParameterType.STRING,
required=False,
label={"en_US": "Other"},
),
]
variable_pool = VariablePool()
variable_pool.add(["node1", "file_var"], FileSegment(value=mock_file))
base_node_data["agent_parameters"] = {
"other_param": AgentNodeData.AgentInput(
type="mixed",
value="{{#node1.file_var#}}",
),
}
node_data = AgentNodeData.model_validate(base_node_data)
agent_parameters = mock_strategy.get_parameters()
agent_node = self._create_agent_node(agent_imports)
# Act
result = agent_node._generate_agent_parameters(
agent_parameters=agent_parameters,
variable_pool=variable_pool,
node_data=node_data,
for_log=False,
strategy=mock_strategy,
)
# Assert
# Non-query/instruction parameters should return text representation
assert isinstance(result["other_param"], str)

View File

@ -6,8 +6,10 @@ import type { StrategyParamItem } from '@/app/components/plugins/types'
import { memo } from 'react' import { memo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toType } from '@/app/components/tools/utils/to-form-schema' import { toType } from '@/app/components/tools/utils/to-form-schema'
import { Resolution } from '@/types/app'
import { useStore } from '../../store' import { useStore } from '../../store'
import { AgentStrategy } from '../_base/components/agent-strategy' import { AgentStrategy } from '../_base/components/agent-strategy'
import ConfigVision from '../_base/components/config-vision'
import Field from '../_base/components/field' import Field from '../_base/components/field'
import MemoryConfig from '../_base/components/memory-config' import MemoryConfig from '../_base/components/memory-config'
import OutputVars, { VarItem } from '../_base/components/output-vars' import OutputVars, { VarItem } from '../_base/components/output-vars'
@ -40,6 +42,8 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
readOnly, readOnly,
outputSchema, outputSchema,
handleMemoryChange, handleMemoryChange,
handleVisionEnabledChange,
handleVisionConfigChange,
canChooseMCPTool, canChooseMCPTool,
} = useConfig(props.id, props.data) } = useConfig(props.id, props.data)
const { t } = useTranslation() const { t } = useTranslation()
@ -85,12 +89,11 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
canChooseMCPTool={canChooseMCPTool} canChooseMCPTool={canChooseMCPTool}
/> />
</Field> </Field>
<div className="px-4 py-2"> <div className="space-y-4 px-4 py-2">
{isChatMode && currentStrategy?.features?.includes(AgentFeature.HISTORY_MESSAGES) && ( {isChatMode && currentStrategy?.features?.includes(AgentFeature.HISTORY_MESSAGES) && (
<> <>
<Split /> <Split />
<MemoryConfig <MemoryConfig
className="mt-4"
readonly={readOnly} readonly={readOnly}
config={{ data: inputs.memory }} config={{ data: inputs.memory }}
onChange={handleMemoryChange} onChange={handleMemoryChange}
@ -98,6 +101,15 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
/> />
</> </>
)} )}
<ConfigVision
nodeId={props.id}
readOnly={readOnly}
isVisionModel={true}
enabled={inputs.vision?.enabled || false}
onEnabledChange={handleVisionEnabledChange}
config={inputs.vision?.configs || { detail: Resolution.high, variable_selector: [] }}
onConfigChange={handleVisionConfigChange}
/>
</div> </div>
<div> <div>
<OutputVars> <OutputVars>

View File

@ -1,6 +1,11 @@
import type { ToolVarInputs } from '../tool/types' import type { ToolVarInputs } from '../tool/types'
import type { PluginMeta } from '@/app/components/plugins/types' import type { PluginMeta } from '@/app/components/plugins/types'
import type { CommonNodeType, Memory } from '@/app/components/workflow/types' import type { CommonNodeType, Memory, VisionSetting } from '@/app/components/workflow/types'
export type AgentVisionConfig = {
enabled: boolean
configs?: VisionSetting
}
export type AgentNodeType = CommonNodeType & { export type AgentNodeType = CommonNodeType & {
agent_strategy_provider_name?: string agent_strategy_provider_name?: string
@ -11,6 +16,7 @@ export type AgentNodeType = CommonNodeType & {
output_schema: Record<string, any> output_schema: Record<string, any>
plugin_unique_identifier?: string plugin_unique_identifier?: string
memory?: Memory memory?: Memory
vision?: AgentVisionConfig
version?: string version?: string
tool_node_version?: string tool_node_version?: string
} }

View File

@ -1,4 +1,4 @@
import type { Memory, Var } from '../../types' import type { Memory, Var, VisionSetting } from '../../types'
import type { ToolVarInputs } from '../tool/types' import type { ToolVarInputs } from '../tool/types'
import type { AgentNodeType } from './types' import type { AgentNodeType } from './types'
import { produce } from 'immer' import { produce } from 'immer'
@ -11,6 +11,7 @@ import {
} from '@/app/components/workflow/hooks' } from '@/app/components/workflow/hooks'
import { useCheckInstalled, useFetchPluginsInMarketPlaceByIds } from '@/service/use-plugins' import { useCheckInstalled, useFetchPluginsInMarketPlaceByIds } from '@/service/use-plugins'
import { useStrategyProviderDetail } from '@/service/use-strategy' import { useStrategyProviderDetail } from '@/service/use-strategy'
import { Resolution } from '@/types/app'
import { isSupportMCP } from '@/utils/plugin-version-feature' import { isSupportMCP } from '@/utils/plugin-version-feature'
import { VarType as VarKindType } from '../../types' import { VarType as VarKindType } from '../../types'
import useAvailableVarList from '../_base/hooks/use-available-var-list' import useAvailableVarList from '../_base/hooks/use-available-var-list'
@ -204,7 +205,34 @@ const useConfig = (id: string, payload: AgentNodeType) => {
}) })
setInputs(newInputs) setInputs(newInputs)
}, [inputs, setInputs]) }, [inputs, setInputs])
const isChatMode = useIsChatMode() const isChatMode = useIsChatMode()
const handleVisionEnabledChange = useCallback((enabled: boolean) => {
const newInputs = produce(inputs, (draft) => {
if (!draft.vision) {
draft.vision = { enabled: false }
}
draft.vision.enabled = enabled
if (enabled && isChatMode) {
draft.vision.configs = {
detail: Resolution.high,
variable_selector: ['sys', 'files'],
}
}
})
setInputs(newInputs)
}, [inputs, setInputs, isChatMode])
const handleVisionConfigChange = useCallback((config: VisionSetting) => {
const newInputs = produce(inputs, (draft) => {
if (!draft.vision) {
draft.vision = { enabled: true }
}
draft.vision.configs = config
})
setInputs(newInputs)
}, [inputs, setInputs])
return { return {
readOnly, readOnly,
inputs, inputs,
@ -221,6 +249,8 @@ const useConfig = (id: string, payload: AgentNodeType) => {
availableNodesWithParent, availableNodesWithParent,
outputSchema, outputSchema,
handleMemoryChange, handleMemoryChange,
handleVisionEnabledChange,
handleVisionConfigChange,
isChatMode, isChatMode,
canChooseMCPTool: isSupportMCP(inputs.meta?.version), canChooseMCPTool: isSupportMCP(inputs.meta?.version),
} }

View File

@ -1024,6 +1024,7 @@ const translation = {
modelSelectorTooltips: { modelSelectorTooltips: {
deprecated: 'تم إهمال هذا النموذج', deprecated: 'تم إهمال هذا النموذج',
}, },
vision: 'الرؤية',
outputVars: { outputVars: {
text: 'محتوى تم إنشاؤه بواسطة الوكيل', text: 'محتوى تم إنشاؤه بواسطة الوكيل',
usage: 'معلومات استخدام النموذج', usage: 'معلومات استخدام النموذج',

View File

@ -901,6 +901,7 @@ const translation = {
modelSelectorTooltips: { modelSelectorTooltips: {
deprecated: 'Dieses Modell ist veraltet', deprecated: 'Dieses Modell ist veraltet',
}, },
vision: 'Vision',
outputVars: { outputVars: {
files: { files: {
type: 'Art der Unterstützung. Jetzt nur noch Image unterstützen', type: 'Art der Unterstützung. Jetzt nur noch Image unterstützen',

View File

@ -1024,6 +1024,7 @@ const translation = {
modelSelectorTooltips: { modelSelectorTooltips: {
deprecated: 'This model is deprecated', deprecated: 'This model is deprecated',
}, },
vision: 'vision',
outputVars: { outputVars: {
text: 'agent generated content', text: 'agent generated content',
usage: 'Model Usage Information', usage: 'Model Usage Information',

View File

@ -901,6 +901,7 @@ const translation = {
modelSelectorTooltips: { modelSelectorTooltips: {
deprecated: 'Este modelo está en desuso', deprecated: 'Este modelo está en desuso',
}, },
vision: 'visión',
outputVars: { outputVars: {
files: { files: {
url: 'URL de la imagen', url: 'URL de la imagen',

View File

@ -901,6 +901,7 @@ const translation = {
modelSelectorTooltips: { modelSelectorTooltips: {
deprecated: 'این مدل منسوخ شده است', deprecated: 'این مدل منسوخ شده است',
}, },
vision: 'بینایی',
outputVars: { outputVars: {
files: { files: {
transfer_method: 'روش انتقال. ارزش remote_url یا local_file', transfer_method: 'روش انتقال. ارزش remote_url یا local_file',

View File

@ -901,6 +901,7 @@ const translation = {
modelSelectorTooltips: { modelSelectorTooltips: {
deprecated: 'Ce modèle est obsolète', deprecated: 'Ce modèle est obsolète',
}, },
vision: 'vision',
outputVars: { outputVars: {
files: { files: {
title: 'Fichiers générés par lagent', title: 'Fichiers générés par lagent',

View File

@ -921,6 +921,7 @@ const translation = {
modelSelectorTooltips: { modelSelectorTooltips: {
deprecated: 'यह मॉडल अप्रचलित है।', deprecated: 'यह मॉडल अप्रचलित है।',
}, },
vision: 'दृष्टि',
outputVars: { outputVars: {
files: { files: {
transfer_method: 'स्थानांतरण विधि। मान या तो remote_url है या local_file।', transfer_method: 'स्थानांतरण विधि। मान या तो remote_url है या local_file।',

View File

@ -940,6 +940,7 @@ const translation = {
modelSelectorTooltips: { modelSelectorTooltips: {
deprecated: 'Model ini tidak digunakan lagi', deprecated: 'Model ini tidak digunakan lagi',
}, },
vision: 'penglihatan',
outputVars: { outputVars: {
files: { files: {
transfer_method: 'Metode transfer. Nilai adalah remote_url atau local_file', transfer_method: 'Metode transfer. Nilai adalah remote_url atau local_file',

View File

@ -927,6 +927,7 @@ const translation = {
modelSelectorTooltips: { modelSelectorTooltips: {
deprecated: 'Questo modello è deprecato', deprecated: 'Questo modello è deprecato',
}, },
vision: 'vision',
outputVars: { outputVars: {
files: { files: {
type: 'Tipo di supporto. Ora supporta solo l\'immagine', type: 'Tipo di supporto. Ora supporta solo l\'immagine',

View File

@ -960,6 +960,7 @@ const translation = {
modelSelectorTooltips: { modelSelectorTooltips: {
deprecated: 'このモデルは廃止されました', deprecated: 'このモデルは廃止されました',
}, },
vision: 'ビジョン',
outputVars: { outputVars: {
files: { files: {
url: '画像の URL', url: '画像の URL',

View File

@ -943,6 +943,7 @@ const translation = {
modelSelectorTooltips: { modelSelectorTooltips: {
deprecated: '이 모델은 더 이상 사용되지 않습니다.', deprecated: '이 모델은 더 이상 사용되지 않습니다.',
}, },
vision: '비전',
outputVars: { outputVars: {
files: { files: {
url: '이미지 URL', url: '이미지 URL',

View File

@ -901,6 +901,7 @@ const translation = {
modelSelectorTooltips: { modelSelectorTooltips: {
deprecated: 'Ten model jest przestarzały', deprecated: 'Ten model jest przestarzały',
}, },
vision: 'wizja',
outputVars: { outputVars: {
files: { files: {
title: 'Pliki generowane przez agenta', title: 'Pliki generowane przez agenta',

View File

@ -901,6 +901,7 @@ const translation = {
modelSelectorTooltips: { modelSelectorTooltips: {
deprecated: 'Este modelo está obsoleto', deprecated: 'Este modelo está obsoleto',
}, },
vision: 'visão',
outputVars: { outputVars: {
files: { files: {
type: 'Tipo de suporte. Agora suporta apenas imagem', type: 'Tipo de suporte. Agora suporta apenas imagem',

View File

@ -901,6 +901,7 @@ const translation = {
modelSelectorTooltips: { modelSelectorTooltips: {
deprecated: 'Acest model este învechit', deprecated: 'Acest model este învechit',
}, },
vision: 'viziune',
outputVars: { outputVars: {
files: { files: {
upload_file_id: 'Încărcați ID-ul fișierului', upload_file_id: 'Încărcați ID-ul fișierului',

View File

@ -901,6 +901,7 @@ const translation = {
modelSelectorTooltips: { modelSelectorTooltips: {
deprecated: 'Эта модель устарела', deprecated: 'Эта модель устарела',
}, },
vision: 'зрение',
outputVars: { outputVars: {
files: { files: {
transfer_method: 'Способ переноса. Ценность составляет remote_url или local_file', transfer_method: 'Способ переноса. Ценность составляет remote_url или local_file',

View File

@ -940,6 +940,7 @@ const translation = {
modelSelectorTooltips: { modelSelectorTooltips: {
deprecated: 'Ta model je zastarelo', deprecated: 'Ta model je zastarelo',
}, },
vision: 'vizija',
outputVars: { outputVars: {
files: { files: {
type: 'Vrsta podpore. Zdaj podpiramo samo slike.', type: 'Vrsta podpore. Zdaj podpiramo samo slike.',

View File

@ -901,6 +901,7 @@ const translation = {
modelSelectorTooltips: { modelSelectorTooltips: {
deprecated: 'โมเดลนี้เลิกใช้แล้ว', deprecated: 'โมเดลนี้เลิกใช้แล้ว',
}, },
vision: 'การมองเห็น',
outputVars: { outputVars: {
files: { files: {
transfer_method: 'วิธีการโอน ค่าเป็น remote_url หรือ local_file', transfer_method: 'วิธีการโอน ค่าเป็น remote_url หรือ local_file',

View File

@ -901,6 +901,7 @@ const translation = {
modelSelectorTooltips: { modelSelectorTooltips: {
deprecated: 'Bu model kullanım dışıdır', deprecated: 'Bu model kullanım dışıdır',
}, },
vision: 'görsel',
outputVars: { outputVars: {
files: { files: {
upload_file_id: 'Dosya kimliğini karşıya yükle', upload_file_id: 'Dosya kimliğini karşıya yükle',

View File

@ -901,6 +901,7 @@ const translation = {
modelSelectorTooltips: { modelSelectorTooltips: {
deprecated: 'Ця модель вважається застарілою', deprecated: 'Ця модель вважається застарілою',
}, },
vision: 'бачення',
outputVars: { outputVars: {
files: { files: {
upload_file_id: 'Завантажити ідентифікатор файлу', upload_file_id: 'Завантажити ідентифікатор файлу',

View File

@ -901,6 +901,7 @@ const translation = {
modelSelectorTooltips: { modelSelectorTooltips: {
deprecated: 'Mô hình này không còn được dùng nữa', deprecated: 'Mô hình này không còn được dùng nữa',
}, },
vision: 'tầm nhìn',
outputVars: { outputVars: {
files: { files: {
title: 'Tệp do tác nhân tạo', title: 'Tệp do tác nhân tạo',

View File

@ -980,6 +980,7 @@ const translation = {
modelSelectorTooltips: { modelSelectorTooltips: {
deprecated: '此模型已弃用', deprecated: '此模型已弃用',
}, },
vision: '视觉',
outputVars: { outputVars: {
text: 'agent 生成的内容', text: 'agent 生成的内容',
usage: '模型用量信息', usage: '模型用量信息',

View File

@ -906,6 +906,7 @@ const translation = {
modelSelectorTooltips: { modelSelectorTooltips: {
deprecated: '此模型已棄用', deprecated: '此模型已棄用',
}, },
vision: '視覺',
outputVars: { outputVars: {
files: { files: {
type: '支撐類型。現在僅支援鏡像', type: '支撐類型。現在僅支援鏡像',