Compare commits

...

16 Commits

Author SHA1 Message Date
zhsama
d8765c8c16 Merge branch 'main' into feat/support-agent-sandbox 2026-02-10 01:03:44 +08:00
zhsama
f6b0fda9f7 Merge branch 'zhsama/remove-reasoning-ui' into feat/support-agent-sandbox 2026-02-10 00:59:08 +08:00
zhsama
f359bbc5de Merge branch 'zhsama/structured-var-search' into feat/support-agent-sandbox 2026-02-10 00:56:14 +08:00
zhsama
7067b5f3cb chore: eslint suppressions 2026-02-10 00:55:44 +08:00
zhsama
d47bc3abc4 feat: Thread nodeOutputVars through HITL and workflow variable components 2026-02-10 00:55:06 +08:00
zhsama
fff2c11d9c refactor(web): remove LLM reasoning format config and clean up locale strings 2026-02-09 23:57:27 +08:00
zhsama
001950d9f8 feat(workflow): support nested variable path search in variable reference picker 2026-02-09 23:50:08 +08:00
Shuvam Pandey
7fb6e0cdfe refactor(api): tighten OTel decorator typing (#32163) 2026-02-10 00:46:02 +09:00
zhsama
41b218f427 feat: LLM node to only show generation output var when computer use is
enabled, matching the actual output structure.
2026-02-09 23:40:42 +08:00
zhsama
a71f336ee0 chore: Add chat history display name mapping for workflow variables 2026-02-09 23:24:53 +08:00
zhsama
cce7970f77 chore: Rename context variable to Chat History in UI 2026-02-09 23:24:53 +08:00
Harry
9614fe8e6e feat: agent sandbox support HITL 2026-02-09 22:20:41 +08:00
Harry
5eeb6c56f0 feat: implement sandbox layer integration in app generators 2026-02-09 22:20:41 +08:00
zhsama
fcf2a334d2 Merge remote-tracking branch 'origin/feat/support-agent-sandbox' into feat/support-agent-sandbox 2026-02-09 21:08:10 +08:00
Harry
f80b5a9537 fix: update SSH VM label and description for clarity 2026-02-09 20:42:13 +08:00
zhsama
defa99e6cd Revert "chore: Update prompt editor context labels to Chat History"
This reverts commit ecf4c06ed7.
2026-02-09 20:23:03 +08:00
68 changed files with 497 additions and 366 deletions

View File

@@ -15,6 +15,7 @@ from sqlalchemy.orm import Session, sessionmaker
import contexts
from configs import dify_config
from constants import UUID_NIL
from core.app.layers.sandbox_layer import SandboxLayer
if TYPE_CHECKING:
from controllers.console.app.workflow import LoopNodeRunPayload
@@ -30,7 +31,6 @@ from core.app.apps.message_based_app_queue_manager import MessageBasedAppQueueMa
from core.app.entities.app_invoke_entities import AdvancedChatAppGenerateEntity, InvokeFrom
from core.app.entities.task_entities import ChatbotAppBlockingResponse, ChatbotAppStreamResponse
from core.app.layers.pause_state_persist_layer import PauseStateLayerConfig, PauseStatePersistenceLayer
from core.app.layers.sandbox_layer import SandboxLayer
from core.helper.trace_id_helper import extract_external_trace_id_from_args
from core.model_runtime.errors.invoke import InvokeAuthorizationError
from core.ops.ops_trace_manager import TraceQueueManager
@@ -500,6 +500,27 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
state_owner_user_id=pause_state_config.state_owner_user_id,
)
)
sandbox: Sandbox | None = None
if workflow.get_feature(WorkflowFeatures.SANDBOX).enabled:
sandbox_provider = SandboxProviderService.get_sandbox_provider(
application_generate_entity.app_config.tenant_id
)
if workflow.version == Workflow.VERSION_DRAFT:
sandbox = SandboxService.create_draft(
tenant_id=application_generate_entity.app_config.tenant_id,
app_id=application_generate_entity.app_config.app_id,
user_id=application_generate_entity.user_id,
sandbox_provider=sandbox_provider,
)
else:
sandbox = SandboxService.create(
tenant_id=application_generate_entity.app_config.tenant_id,
app_id=application_generate_entity.app_config.app_id,
user_id=application_generate_entity.user_id,
sandbox_id=conversation.id,
sandbox_provider=sandbox_provider,
)
graph_layers.append(SandboxLayer(sandbox))
# new thread with request context and contextvars
context = contextvars.copy_context()
@@ -518,6 +539,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
"workflow_node_execution_repository": workflow_node_execution_repository,
"graph_engine_layers": tuple(graph_layers),
"graph_runtime_state": graph_runtime_state,
"sandbox": sandbox,
},
)
@@ -565,6 +587,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
workflow_node_execution_repository: WorkflowNodeExecutionRepository,
graph_engine_layers: Sequence[GraphEngineLayer] = (),
graph_runtime_state: GraphRuntimeState | None = None,
sandbox: Sandbox | None = None,
):
"""
Generate worker in a new thread.
@@ -592,29 +615,6 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
if workflow is None:
raise ValueError("Workflow not found")
sandbox: Sandbox | None = None
graph_engine_layers: tuple = ()
if workflow.get_feature(WorkflowFeatures.SANDBOX).enabled:
sandbox_provider = SandboxProviderService.get_sandbox_provider(
application_generate_entity.app_config.tenant_id
)
if workflow.version == Workflow.VERSION_DRAFT:
sandbox = SandboxService.create_draft(
tenant_id=application_generate_entity.app_config.tenant_id,
app_id=application_generate_entity.app_config.app_id,
user_id=application_generate_entity.user_id,
sandbox_provider=sandbox_provider,
)
else:
sandbox = SandboxService.create(
tenant_id=application_generate_entity.app_config.tenant_id,
app_id=application_generate_entity.app_config.app_id,
user_id=application_generate_entity.user_id,
sandbox_id=conversation_id,
sandbox_provider=sandbox_provider,
)
graph_engine_layers = (SandboxLayer(sandbox=sandbox),)
# Determine system_user_id based on invocation source
is_external_api_call = application_generate_entity.invoke_from in {
InvokeFrom.WEB_APP,

View File

@@ -119,6 +119,7 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner):
if resume_state is not None:
graph_runtime_state = resume_state
graph_runtime_state.set_sandbox(self._sandbox)
variable_pool = graph_runtime_state.variable_pool
graph = self._init_graph(
graph_config=self._workflow.graph_dict,
@@ -175,9 +176,7 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner):
# init graph
graph_runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=time.time())
if self._sandbox:
graph_runtime_state.set_sandbox(self._sandbox)
graph_runtime_state.set_sandbox(self._sandbox)
graph = self._init_graph(
graph_config=self._workflow.graph_dict,

View File

@@ -32,7 +32,7 @@ from core.helper.trace_id_helper import extract_external_trace_id_from_args
from core.model_runtime.errors.invoke import InvokeAuthorizationError
from core.ops.ops_trace_manager import TraceQueueManager
from core.repositories import DifyCoreRepositoryFactory
from core.sandbox import Sandbox
from core.sandbox.sandbox import Sandbox
from core.workflow.graph_engine.layers.base import GraphEngineLayer
from core.workflow.repositories.draft_variable_repository import DraftVariableSaverFactory
from core.workflow.repositories.workflow_execution_repository import WorkflowExecutionRepository
@@ -316,6 +316,28 @@ class WorkflowAppGenerator(BaseAppGenerator):
)
)
sandbox: Sandbox | None = None
if workflow.get_feature(WorkflowFeatures.SANDBOX).enabled:
sandbox_provider = SandboxProviderService.get_sandbox_provider(
application_generate_entity.app_config.tenant_id
)
if workflow.version == Workflow.VERSION_DRAFT:
sandbox = SandboxService.create_draft(
tenant_id=application_generate_entity.app_config.tenant_id,
app_id=application_generate_entity.app_config.app_id,
user_id=application_generate_entity.user_id,
sandbox_provider=sandbox_provider,
)
else:
sandbox = SandboxService.create(
tenant_id=application_generate_entity.app_config.tenant_id,
app_id=application_generate_entity.app_config.app_id,
user_id=application_generate_entity.user_id,
sandbox_id=application_generate_entity.workflow_execution_id,
sandbox_provider=sandbox_provider,
)
graph_layers.append(SandboxLayer(sandbox=sandbox))
# new thread with request context and contextvars
context = contextvars.copy_context()
@@ -335,6 +357,7 @@ class WorkflowAppGenerator(BaseAppGenerator):
"workflow_node_execution_repository": workflow_node_execution_repository,
"graph_engine_layers": tuple(graph_layers),
"graph_runtime_state": graph_runtime_state,
"sandbox": sandbox,
},
)
@@ -533,6 +556,7 @@ class WorkflowAppGenerator(BaseAppGenerator):
root_node_id: str | None = None,
graph_engine_layers: Sequence[GraphEngineLayer] = (),
graph_runtime_state: GraphRuntimeState | None = None,
sandbox: Sandbox | None = None,
) -> None:
"""
Generate worker in a new thread.
@@ -554,31 +578,6 @@ class WorkflowAppGenerator(BaseAppGenerator):
if workflow is None:
raise ValueError("Workflow not found")
sandbox: Sandbox | None = None
if workflow.get_feature(WorkflowFeatures.SANDBOX).enabled:
sandbox_provider = SandboxProviderService.get_sandbox_provider(
application_generate_entity.app_config.tenant_id
)
if workflow.version == Workflow.VERSION_DRAFT:
sandbox = SandboxService.create_draft(
tenant_id=application_generate_entity.app_config.tenant_id,
app_id=application_generate_entity.app_config.app_id,
user_id=application_generate_entity.user_id,
sandbox_provider=sandbox_provider,
)
else:
sandbox = SandboxService.create(
tenant_id=application_generate_entity.app_config.tenant_id,
app_id=application_generate_entity.app_config.app_id,
user_id=application_generate_entity.user_id,
sandbox_id=application_generate_entity.workflow_execution_id,
sandbox_provider=sandbox_provider,
)
graph_engine_layers = (
*graph_engine_layers,
SandboxLayer(sandbox=sandbox),
)
# Determine system_user_id based on invocation source
is_external_api_call = application_generate_entity.invoke_from in {
InvokeFrom.WEB_APP,

View File

@@ -78,6 +78,7 @@ class WorkflowAppRunner(WorkflowBasedAppRunner):
if resume_state is not None:
graph_runtime_state = resume_state
graph_runtime_state.set_sandbox(self._sandbox)
variable_pool = graph_runtime_state.variable_pool
graph = self._init_graph(
graph_config=self._workflow.graph_dict,
@@ -115,9 +116,7 @@ class WorkflowAppRunner(WorkflowBasedAppRunner):
)
graph_runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter())
if self._sandbox:
graph_runtime_state.set_sandbox(self._sandbox)
graph_runtime_state.set_sandbox(self._sandbox)
# init graph
graph = self._init_graph(

View File

@@ -355,7 +355,7 @@ class GraphRuntimeState:
def sandbox(self) -> Sandbox | None:
return self._sandbox
def set_sandbox(self, sandbox: Sandbox) -> None:
def set_sandbox(self, sandbox: Sandbox | None) -> None:
self._sandbox = sandbox
# ------------------------------------------------------------------

View File

@@ -1,6 +1,6 @@
import functools
from collections.abc import Callable
from typing import Any, TypeVar, cast
from typing import ParamSpec, TypeVar, cast
from opentelemetry.trace import get_tracer
@@ -8,7 +8,8 @@ from configs import dify_config
from extensions.otel.decorators.handler import SpanHandler
from extensions.otel.runtime import is_instrument_flag_enabled
T = TypeVar("T", bound=Callable[..., Any])
P = ParamSpec("P")
R = TypeVar("R")
_HANDLER_INSTANCES: dict[type[SpanHandler], SpanHandler] = {SpanHandler: SpanHandler()}
@@ -20,7 +21,7 @@ def _get_handler_instance(handler_class: type[SpanHandler]) -> SpanHandler:
return _HANDLER_INSTANCES[handler_class]
def trace_span(handler_class: type[SpanHandler] | None = None) -> Callable[[T], T]:
def trace_span(handler_class: type[SpanHandler] | None = None) -> Callable[[Callable[P, R]], Callable[P, R]]:
"""
Decorator that traces a function with an OpenTelemetry span.
@@ -30,9 +31,9 @@ def trace_span(handler_class: type[SpanHandler] | None = None) -> Callable[[T],
:param handler_class: Optional handler class to use for this span. If None, uses the default SpanHandler.
"""
def decorator(func: T) -> T:
def decorator(func: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
if not (dify_config.ENABLE_OTEL or is_instrument_flag_enabled()):
return func(*args, **kwargs)
@@ -46,6 +47,6 @@ def trace_span(handler_class: type[SpanHandler] | None = None) -> Callable[[T],
kwargs=kwargs,
)
return cast(T, wrapper)
return cast(Callable[P, R], wrapper)
return decorator

View File

@@ -1,9 +1,11 @@
import inspect
from collections.abc import Callable, Mapping
from typing import Any
from typing import Any, TypeVar
from opentelemetry.trace import SpanKind, Status, StatusCode
R = TypeVar("R")
class SpanHandler:
"""
@@ -31,9 +33,9 @@ class SpanHandler:
def _extract_arguments(
self,
wrapped: Callable[..., Any],
args: tuple[Any, ...],
kwargs: Mapping[str, Any],
wrapped: Callable[..., R],
args: tuple[object, ...],
kwargs: Mapping[str, object],
) -> dict[str, Any] | None:
"""
Extract function arguments using inspect.signature.
@@ -62,10 +64,10 @@ class SpanHandler:
def wrapper(
self,
tracer: Any,
wrapped: Callable[..., Any],
args: tuple[Any, ...],
kwargs: Mapping[str, Any],
) -> Any:
wrapped: Callable[..., R],
args: tuple[object, ...],
kwargs: Mapping[str, object],
) -> R:
"""
Fully control the wrapper behavior.

View File

@@ -1,6 +1,6 @@
import logging
from collections.abc import Callable, Mapping
from typing import Any
from typing import Any, TypeVar
from opentelemetry.trace import SpanKind, Status, StatusCode
from opentelemetry.util.types import AttributeValue
@@ -12,16 +12,19 @@ from models.model import Account
logger = logging.getLogger(__name__)
R = TypeVar("R")
class AppGenerateHandler(SpanHandler):
"""Span handler for ``AppGenerateService.generate``."""
def wrapper(
self,
tracer: Any,
wrapped: Callable[..., Any],
args: tuple[Any, ...],
kwargs: Mapping[str, Any],
) -> Any:
wrapped: Callable[..., R],
args: tuple[object, ...],
kwargs: Mapping[str, object],
) -> R:
try:
arguments = self._extract_arguments(wrapped, args, kwargs)
if not arguments:

View File

@@ -3,7 +3,7 @@ import type { FC } from 'react'
import type { WorkflowNodesMap } from '../workflow-variable-block/node'
import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types'
import type { Type } from '@/app/components/workflow/nodes/llm/types'
import type { ValueSelector, Var } from '@/app/components/workflow/types'
import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
import { RiDeleteBinLine, RiEditLine } from '@remixicon/react'
import { useBoolean } from 'ahooks'
import * as React from 'react'
@@ -23,6 +23,7 @@ type HITLInputComponentUIProps = {
onRename: (payload: FormInputItem, oldName: string) => void
onRemove: (varName: string) => void
workflowNodesMap: WorkflowNodesMap
nodeOutputVars?: NodeOutPutVar[]
environmentVariables?: Var[]
conversationVariables?: Var[]
ragVariables?: Var[]
@@ -49,6 +50,7 @@ const HITLInputComponentUI: FC<HITLInputComponentUIProps> = ({
onRename,
onRemove,
workflowNodesMap = {},
nodeOutputVars,
getVarType,
environmentVariables,
conversationVariables,
@@ -118,6 +120,7 @@ const HITLInputComponentUI: FC<HITLInputComponentUIProps> = ({
<VariableBlock
variables={formInput.default?.selector}
workflowNodesMap={workflowNodesMap}
nodeOutputVars={nodeOutputVars}
getVarType={getVarType}
environmentVariables={environmentVariables}
conversationVariables={conversationVariables}

View File

@@ -2,7 +2,7 @@ import type { FC } from 'react'
import type { WorkflowNodesMap } from '../workflow-variable-block/node'
import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types'
import type { Type } from '@/app/components/workflow/nodes/llm/types'
import type { ValueSelector, Var } from '@/app/components/workflow/types'
import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
import { produce } from 'immer'
import { useCallback } from 'react'
import { useSelectOrDelete } from '../../hooks'
@@ -18,6 +18,7 @@ type HITLInputComponentProps = {
onRename: (payload: FormInputItem, oldName: string) => void
onRemove: (varName: string) => void
workflowNodesMap: WorkflowNodesMap
nodeOutputVars?: NodeOutPutVar[]
environmentVariables?: Var[]
conversationVariables?: Var[]
ragVariables?: Var[]
@@ -37,6 +38,7 @@ const HITLInputComponent: FC<HITLInputComponentProps> = ({
onRename,
onRemove,
workflowNodesMap = {},
nodeOutputVars,
getVarType,
environmentVariables,
conversationVariables,
@@ -73,6 +75,7 @@ const HITLInputComponent: FC<HITLInputComponentProps> = ({
onRename={onRename}
onRemove={onRemove}
workflowNodesMap={workflowNodesMap}
nodeOutputVars={nodeOutputVars}
getVarType={getVarType}
environmentVariables={environmentVariables}
conversationVariables={conversationVariables}

View File

@@ -55,6 +55,7 @@ const HITLInputReplacementBlock = ({
onFormInputItemRemove!,
workflowNodesMap,
getVarType,
variables,
environmentVariables,
conversationVariables,
ragVariables,

View File

@@ -1,4 +1,5 @@
import type { HITLInputBlockType } from '../../types'
import type { UpdateWorkflowNodesMapPayload } from '../workflow-variable-block'
import type {
HITLNodeProps,
} from './node'
@@ -14,6 +15,7 @@ import {
useEffect,
} from 'react'
import { CustomTextNode } from '../custom-text/node'
import { UPDATE_WORKFLOW_NODES_MAP } from '../workflow-variable-block'
import {
$createHITLInputNode,
HITLInputNode,
@@ -21,7 +23,6 @@ import {
export const INSERT_HITL_INPUT_BLOCK_COMMAND = createCommand('INSERT_HITL_INPUT_BLOCK_COMMAND')
export const DELETE_HITL_INPUT_BLOCK_COMMAND = createCommand('DELETE_HITL_INPUT_BLOCK_COMMAND')
export const UPDATE_WORKFLOW_NODES_MAP = createCommand('UPDATE_WORKFLOW_NODES_MAP')
export type HITLInputProps = {
onInsert?: () => void
@@ -31,6 +32,7 @@ const HITLInputBlock = memo(({
onInsert,
onDelete,
workflowNodesMap,
variables,
getVarType,
readonly,
}: HITLInputBlockType) => {
@@ -38,9 +40,13 @@ const HITLInputBlock = memo(({
useEffect(() => {
editor.update(() => {
editor.dispatchCommand(UPDATE_WORKFLOW_NODES_MAP, workflowNodesMap)
const payload: UpdateWorkflowNodesMapPayload = {
workflowNodesMap: workflowNodesMap || {},
nodeOutputVars: variables || [],
}
editor.dispatchCommand(UPDATE_WORKFLOW_NODES_MAP, payload)
})
}, [editor, workflowNodesMap])
}, [editor, workflowNodesMap, variables])
useEffect(() => {
if (!editor.hasNodes([HITLInputNode]))
@@ -66,6 +72,7 @@ const HITLInputBlock = memo(({
onFormInputItemRemove,
workflowNodesMap,
getVarType,
variables,
undefined,
undefined,
undefined,
@@ -94,7 +101,7 @@ const HITLInputBlock = memo(({
COMMAND_PRIORITY_EDITOR,
),
)
}, [editor, onInsert, onDelete])
}, [editor, onInsert, onDelete, workflowNodesMap, getVarType, variables, readonly])
return null
})

View File

@@ -2,7 +2,7 @@ import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
import type { GetVarType } from '../../types'
import type { WorkflowNodesMap } from '../workflow-variable-block/node'
import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types'
import type { Var } from '@/app/components/workflow/types'
import type { NodeOutPutVar, Var } from '@/app/components/workflow/types'
import { DecoratorNode } from 'lexical'
import HILTInputBlockComponent from './component'
@@ -15,6 +15,7 @@ export type HITLNodeProps = {
onFormInputItemRemove: (varName: string) => void
workflowNodesMap: WorkflowNodesMap
getVarType?: GetVarType
nodeOutputVars?: NodeOutPutVar[]
environmentVariables?: Var[]
conversationVariables?: Var[]
ragVariables?: Var[]
@@ -32,6 +33,7 @@ export class HITLInputNode extends DecoratorNode<React.JSX.Element> {
__onFormInputItemRemove: (varName: string) => void
__workflowNodesMap: WorkflowNodesMap
__getVarType?: GetVarType
__nodeOutputVars?: NodeOutPutVar[]
__environmentVariables?: Var[]
__conversationVariables?: Var[]
__ragVariables?: Var[]
@@ -89,6 +91,11 @@ export class HITLInputNode extends DecoratorNode<React.JSX.Element> {
return self.__getVarType
}
getNodeOutputVars(): NodeOutPutVar[] {
const self = this.getLatest()
return self.__nodeOutputVars || []
}
getEnvironmentVariables(): Var[] {
const self = this.getLatest()
return self.__environmentVariables || []
@@ -119,6 +126,7 @@ export class HITLInputNode extends DecoratorNode<React.JSX.Element> {
node.__onFormInputItemRemove,
node.__workflowNodesMap,
node.__getVarType,
node.__nodeOutputVars,
node.__environmentVariables,
node.__conversationVariables,
node.__ragVariables,
@@ -140,6 +148,7 @@ export class HITLInputNode extends DecoratorNode<React.JSX.Element> {
onFormInputItemRemove: (varName: string) => void,
workflowNodesMap: WorkflowNodesMap,
getVarType?: GetVarType,
nodeOutputVars?: NodeOutPutVar[],
environmentVariables?: Var[],
conversationVariables?: Var[],
ragVariables?: Var[],
@@ -156,6 +165,7 @@ export class HITLInputNode extends DecoratorNode<React.JSX.Element> {
this.__onFormInputItemRemove = onFormInputItemRemove
this.__workflowNodesMap = workflowNodesMap
this.__getVarType = getVarType
this.__nodeOutputVars = nodeOutputVars
this.__environmentVariables = environmentVariables
this.__conversationVariables = conversationVariables
this.__ragVariables = ragVariables
@@ -184,6 +194,7 @@ export class HITLInputNode extends DecoratorNode<React.JSX.Element> {
onRemove={this.getOnFormInputItemRemove()}
workflowNodesMap={this.getWorkflowNodesMap()}
getVarType={this.getGetVarType()}
nodeOutputVars={this.getNodeOutputVars()}
environmentVariables={this.getEnvironmentVariables()}
conversationVariables={this.getConversationVariables()}
ragVariables={this.getRagVariables()}
@@ -202,6 +213,7 @@ export class HITLInputNode extends DecoratorNode<React.JSX.Element> {
serializedNode.onFormInputItemRemove,
serializedNode.workflowNodesMap,
serializedNode.getVarType,
serializedNode.nodeOutputVars,
serializedNode.environmentVariables,
serializedNode.conversationVariables,
serializedNode.ragVariables,
@@ -223,6 +235,7 @@ export class HITLInputNode extends DecoratorNode<React.JSX.Element> {
onFormInputItemRemove: this.getOnFormInputItemRemove(),
workflowNodesMap: this.getWorkflowNodesMap(),
getVarType: this.getGetVarType(),
nodeOutputVars: this.getNodeOutputVars(),
environmentVariables: this.getEnvironmentVariables(),
conversationVariables: this.getConversationVariables(),
ragVariables: this.getRagVariables(),
@@ -244,6 +257,7 @@ export function $createHITLInputNode(
onFormInputItemRemove: (varName: string) => void,
workflowNodesMap: WorkflowNodesMap,
getVarType?: GetVarType,
nodeOutputVars?: NodeOutPutVar[],
environmentVariables?: Var[],
conversationVariables?: Var[],
ragVariables?: Var[],
@@ -258,6 +272,7 @@ export function $createHITLInputNode(
onFormInputItemRemove,
workflowNodesMap,
getVarType,
nodeOutputVars,
environmentVariables,
conversationVariables,
ragVariables,

View File

@@ -1,5 +1,6 @@
import type { UpdateWorkflowNodesMapPayload } from '../workflow-variable-block'
import type { WorkflowNodesMap } from '../workflow-variable-block/node'
import type { ValueSelector, Var } from '@/app/components/workflow/types'
import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { mergeRegister } from '@lexical/utils'
import {
@@ -19,6 +20,7 @@ import {
isGlobalVar,
isRagVariableVar,
isSystemVar,
isValueSelectorInNodeOutputVars,
} from '@/app/components/workflow/nodes/_base/components/variable/utils'
import VarFullPathPanel from '@/app/components/workflow/nodes/_base/components/variable/var-full-path-panel'
import {
@@ -32,6 +34,7 @@ import { HITLInputNode } from './node'
type HITLInputVariableBlockComponentProps = {
variables: string[]
workflowNodesMap: WorkflowNodesMap
nodeOutputVars?: NodeOutPutVar[]
environmentVariables?: Var[]
conversationVariables?: Var[]
ragVariables?: Var[]
@@ -44,6 +47,7 @@ type HITLInputVariableBlockComponentProps = {
const HITLInputVariableBlockComponent = ({
variables,
workflowNodesMap = {},
nodeOutputVars,
getVarType,
environmentVariables,
conversationVariables,
@@ -62,10 +66,14 @@ const HITLInputVariableBlockComponent = ({
}
)()
const [localWorkflowNodesMap, setLocalWorkflowNodesMap] = useState<WorkflowNodesMap>(workflowNodesMap)
const [localNodeOutputVars, setLocalNodeOutputVars] = useState<NodeOutPutVar[]>(nodeOutputVars || [])
const node = localWorkflowNodesMap![variables[isRagVar ? 1 : 0]]
const isException = isExceptionVariable(varName, node?.type)
const variableValid = useMemo(() => {
if (localNodeOutputVars.length)
return isValueSelectorInNodeOutputVars(variables, localNodeOutputVars)
let variableValid = true
const isEnv = isENV(variables)
const isChatVar = isConversationVar(variables)
@@ -89,7 +97,7 @@ const HITLInputVariableBlockComponent = ({
variableValid = !!node
}
return variableValid
}, [variables, node, environmentVariables, conversationVariables, isRagVar, ragVariables])
}, [variables, node, environmentVariables, conversationVariables, isRagVar, ragVariables, localNodeOutputVars])
useEffect(() => {
if (!editor.hasNodes([HITLInputNode]))
@@ -98,8 +106,9 @@ const HITLInputVariableBlockComponent = ({
return mergeRegister(
editor.registerCommand(
UPDATE_WORKFLOW_NODES_MAP,
(workflowNodesMap: WorkflowNodesMap) => {
setLocalWorkflowNodesMap(workflowNodesMap)
(payload: UpdateWorkflowNodesMapPayload) => {
setLocalWorkflowNodesMap(payload.workflowNodesMap)
setLocalNodeOutputVars(payload.nodeOutputVars)
return true
},

View File

@@ -1,5 +1,6 @@
import type { UpdateWorkflowNodesMapPayload } from './index'
import type { WorkflowNodesMap } from './node'
import type { ValueSelector, Var } from '@/app/components/workflow/types'
import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { mergeRegister } from '@lexical/utils'
import {
@@ -15,7 +16,7 @@ import {
import { useTranslation } from 'react-i18next'
import { useReactFlow, useStoreApi } from 'reactflow'
import Tooltip from '@/app/components/base/tooltip'
import { isConversationVar, isENV, isGlobalVar, isRagVariableVar, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { isConversationVar, isENV, isGlobalVar, isRagVariableVar, isSystemVar, isValueSelectorInNodeOutputVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import VarFullPathPanel from '@/app/components/workflow/nodes/_base/components/variable/var-full-path-panel'
import {
VariableLabelInEditor,
@@ -34,6 +35,7 @@ type WorkflowVariableBlockComponentProps = {
nodeKey: string
variables: string[]
workflowNodesMap: WorkflowNodesMap
nodeOutputVars?: NodeOutPutVar[]
environmentVariables?: Var[]
conversationVariables?: Var[]
ragVariables?: Var[]
@@ -47,6 +49,7 @@ const WorkflowVariableBlockComponent = ({
nodeKey,
variables,
workflowNodesMap = {},
nodeOutputVars,
getVarType,
environmentVariables,
conversationVariables,
@@ -66,12 +69,16 @@ const WorkflowVariableBlockComponent = ({
}
)()
const [localWorkflowNodesMap, setLocalWorkflowNodesMap] = useState<WorkflowNodesMap>(workflowNodesMap)
const [localNodeOutputVars, setLocalNodeOutputVars] = useState<NodeOutPutVar[]>(nodeOutputVars || [])
const node = localWorkflowNodesMap![variables[isRagVar ? 1 : 0]]
const isContextVariable = (node?.type === BlockEnum.Agent || node?.type === BlockEnum.LLM)
&& variables[variablesLength - 1] === 'context'
const isException = isExceptionVariable(varName, node?.type)
const variableValid = useMemo(() => {
if (localNodeOutputVars.length)
return isValueSelectorInNodeOutputVars(variables, localNodeOutputVars)
let variableValid = true
const isEnv = isENV(variables)
const isChatVar = isConversationVar(variables)
@@ -95,7 +102,7 @@ const WorkflowVariableBlockComponent = ({
variableValid = !!node
}
return variableValid
}, [variables, node, environmentVariables, conversationVariables, isRagVar, ragVariables])
}, [variables, node, environmentVariables, conversationVariables, isRagVar, ragVariables, localNodeOutputVars])
const reactflow = useReactFlow()
const store = useStoreApi()
@@ -107,8 +114,9 @@ const WorkflowVariableBlockComponent = ({
return mergeRegister(
editor.registerCommand(
UPDATE_WORKFLOW_NODES_MAP,
(workflowNodesMap: WorkflowNodesMap) => {
setLocalWorkflowNodesMap(workflowNodesMap)
(payload: UpdateWorkflowNodesMapPayload) => {
setLocalWorkflowNodesMap(payload.workflowNodesMap)
setLocalNodeOutputVars(payload.nodeOutputVars)
return true
},

View File

@@ -19,7 +19,13 @@ import {
export const INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND = createCommand('INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND')
export const DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND = createCommand('DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND')
export const CLEAR_HIDE_MENU_TIMEOUT = createCommand('CLEAR_HIDE_MENU_TIMEOUT')
export const UPDATE_WORKFLOW_NODES_MAP = createCommand('UPDATE_WORKFLOW_NODES_MAP')
export type UpdateWorkflowNodesMapPayload = {
workflowNodesMap: NonNullable<WorkflowVariableBlockType['workflowNodesMap']>
nodeOutputVars: NonNullable<WorkflowVariableBlockType['variables']>
}
export const UPDATE_WORKFLOW_NODES_MAP = createCommand<UpdateWorkflowNodesMapPayload>('UPDATE_WORKFLOW_NODES_MAP')
export type WorkflowVariableBlockProps = {
getWorkflowNode: (nodeId: string) => Node
@@ -29,6 +35,7 @@ export type WorkflowVariableBlockProps = {
}
const WorkflowVariableBlock = memo(({
workflowNodesMap,
variables,
onInsert,
onDelete,
getVarType,
@@ -37,9 +44,12 @@ const WorkflowVariableBlock = memo(({
useEffect(() => {
editor.update(() => {
editor.dispatchCommand(UPDATE_WORKFLOW_NODES_MAP, workflowNodesMap)
editor.dispatchCommand(UPDATE_WORKFLOW_NODES_MAP, {
workflowNodesMap: workflowNodesMap || {},
nodeOutputVars: variables || [],
})
})
}, [editor, workflowNodesMap])
}, [editor, workflowNodesMap, variables])
useEffect(() => {
if (!editor.hasNodes([WorkflowVariableBlockNode]))
@@ -48,9 +58,9 @@ const WorkflowVariableBlock = memo(({
return mergeRegister(
editor.registerCommand(
INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND,
(variables: string[]) => {
(insertedVariables: string[]) => {
editor.dispatchCommand(CLEAR_HIDE_MENU_TIMEOUT, undefined)
const workflowVariableBlockNode = $createWorkflowVariableBlockNode(variables, workflowNodesMap, getVarType)
const workflowVariableBlockNode = $createWorkflowVariableBlockNode(insertedVariables, workflowNodesMap, getVarType, undefined, undefined, undefined, variables)
$insertNodes([workflowVariableBlockNode])
if (onInsert)
@@ -71,7 +81,7 @@ const WorkflowVariableBlock = memo(({
COMMAND_PRIORITY_EDITOR,
),
)
}, [editor, onInsert, onDelete, workflowNodesMap, getVarType])
}, [editor, onInsert, onDelete, workflowNodesMap, getVarType, variables])
return null
})

View File

@@ -1,6 +1,6 @@
import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
import type { GetVarType, WorkflowVariableBlockType } from '../../types'
import type { Var } from '@/app/components/workflow/types'
import type { NodeOutPutVar, Var } from '@/app/components/workflow/types'
import { DecoratorNode } from 'lexical'
import { BlockEnum } from '@/app/components/workflow/types'
import WorkflowVariableBlockComponent from './component'
@@ -11,6 +11,7 @@ export type SerializedNode = SerializedLexicalNode & {
variables: string[]
workflowNodesMap: WorkflowNodesMap
getVarType?: GetVarType
nodeOutputVars?: NodeOutPutVar[]
environmentVariables?: Var[]
conversationVariables?: Var[]
ragVariables?: Var[]
@@ -20,6 +21,7 @@ export class WorkflowVariableBlockNode extends DecoratorNode<React.JSX.Element>
__variables: string[]
__workflowNodesMap: WorkflowNodesMap
__getVarType?: GetVarType
__nodeOutputVars?: NodeOutPutVar[]
__environmentVariables?: Var[]
__conversationVariables?: Var[]
__ragVariables?: Var[]
@@ -29,14 +31,14 @@ export class WorkflowVariableBlockNode extends DecoratorNode<React.JSX.Element>
}
static clone(node: WorkflowVariableBlockNode): WorkflowVariableBlockNode {
return new WorkflowVariableBlockNode(node.__variables, node.__workflowNodesMap, node.__getVarType, node.__key, node.__environmentVariables, node.__conversationVariables, node.__ragVariables)
return new WorkflowVariableBlockNode(node.__variables, node.__workflowNodesMap, node.__getVarType, node.__key, node.__environmentVariables, node.__conversationVariables, node.__ragVariables, node.__nodeOutputVars)
}
isInline(): boolean {
return true
}
constructor(variables: string[], workflowNodesMap: WorkflowNodesMap, getVarType: any, key?: NodeKey, environmentVariables?: Var[], conversationVariables?: Var[], ragVariables?: Var[]) {
constructor(variables: string[], workflowNodesMap: WorkflowNodesMap, getVarType: any, key?: NodeKey, environmentVariables?: Var[], conversationVariables?: Var[], ragVariables?: Var[], nodeOutputVars?: NodeOutPutVar[]) {
super(key)
this.__variables = variables
@@ -45,6 +47,7 @@ export class WorkflowVariableBlockNode extends DecoratorNode<React.JSX.Element>
this.__environmentVariables = environmentVariables
this.__conversationVariables = conversationVariables
this.__ragVariables = ragVariables
this.__nodeOutputVars = nodeOutputVars
}
createDOM(): HTMLElement {
@@ -63,6 +66,7 @@ export class WorkflowVariableBlockNode extends DecoratorNode<React.JSX.Element>
nodeKey={this.getKey()}
variables={this.__variables}
workflowNodesMap={this.__workflowNodesMap}
nodeOutputVars={this.__nodeOutputVars}
getVarType={this.__getVarType!}
environmentVariables={this.__environmentVariables}
conversationVariables={this.__conversationVariables}
@@ -72,7 +76,7 @@ export class WorkflowVariableBlockNode extends DecoratorNode<React.JSX.Element>
}
static importJSON(serializedNode: SerializedNode): WorkflowVariableBlockNode {
const node = $createWorkflowVariableBlockNode(serializedNode.variables, serializedNode.workflowNodesMap, serializedNode.getVarType, serializedNode.environmentVariables, serializedNode.conversationVariables, serializedNode.ragVariables)
const node = $createWorkflowVariableBlockNode(serializedNode.variables, serializedNode.workflowNodesMap, serializedNode.getVarType, serializedNode.environmentVariables, serializedNode.conversationVariables, serializedNode.ragVariables, serializedNode.nodeOutputVars)
return node
}
@@ -84,6 +88,7 @@ export class WorkflowVariableBlockNode extends DecoratorNode<React.JSX.Element>
variables: this.getVariables(),
workflowNodesMap: this.getWorkflowNodesMap(),
getVarType: this.getVarType(),
nodeOutputVars: this.getNodeOutputVars(),
environmentVariables: this.getEnvironmentVariables(),
conversationVariables: this.getConversationVariables(),
ragVariables: this.getRagVariables(),
@@ -105,6 +110,11 @@ export class WorkflowVariableBlockNode extends DecoratorNode<React.JSX.Element>
return self.__getVarType
}
getNodeOutputVars(): NodeOutPutVar[] {
const self = this.getLatest()
return self.__nodeOutputVars || []
}
getEnvironmentVariables(): any {
const self = this.getLatest()
return self.__environmentVariables
@@ -129,8 +139,8 @@ export class WorkflowVariableBlockNode extends DecoratorNode<React.JSX.Element>
return `{{${marker}${variables.join('.')}${marker}}}`
}
}
export function $createWorkflowVariableBlockNode(variables: string[], workflowNodesMap: WorkflowNodesMap, getVarType?: GetVarType, environmentVariables?: Var[], conversationVariables?: Var[], ragVariables?: Var[]): WorkflowVariableBlockNode {
return new WorkflowVariableBlockNode(variables, workflowNodesMap, getVarType, undefined, environmentVariables, conversationVariables, ragVariables)
export function $createWorkflowVariableBlockNode(variables: string[], workflowNodesMap: WorkflowNodesMap, getVarType?: GetVarType, environmentVariables?: Var[], conversationVariables?: Var[], ragVariables?: Var[], nodeOutputVars?: NodeOutPutVar[]): WorkflowVariableBlockNode {
return new WorkflowVariableBlockNode(variables, workflowNodesMap, getVarType, undefined, environmentVariables, conversationVariables, ragVariables, nodeOutputVars)
}
export function $isWorkflowVariableBlockNode(

View File

@@ -39,7 +39,7 @@ const WorkflowVariableBlockReplacementBlock = ({
onInsert()
const nodePathString = textNode.getTextContent().slice(3, -3)
return $applyNodeReplacement($createWorkflowVariableBlockNode(nodePathString.split('.'), workflowNodesMap, getVarType, variables?.find(o => o.nodeId === 'env')?.vars || [], variables?.find(o => o.nodeId === 'conversation')?.vars || [], ragVariables))
return $applyNodeReplacement($createWorkflowVariableBlockNode(nodePathString.split('.'), workflowNodesMap, getVarType, variables?.find(o => o.nodeId === 'env')?.vars || [], variables?.find(o => o.nodeId === 'conversation')?.vars || [], ragVariables, variables))
}, [onInsert, workflowNodesMap, getVarType, variables, ragVariables])
const getMatch = useCallback((text: string) => {

View File

@@ -86,6 +86,7 @@ export const getGlobalVars = (isChatMode: boolean): Var[] => {
export const VAR_SHOW_NAME_MAP: Record<string, string> = {
'sys.query': 'query',
'sys.files': 'files',
'context': 'chat history',
}
export const RETRIEVAL_OUTPUT_STRUCT = `{

View File

@@ -46,7 +46,7 @@ import {
useGetToolIcon,
useNodesMetaData,
} from '../hooks'
import { getNodeUsedVars, isSpecialVar } from '../nodes/_base/components/variable/utils'
import { getNodeUsedVars, isValueSelectorInNodeOutputVars } from '../nodes/_base/components/variable/utils'
import {
useStore,
useWorkflowStore,
@@ -186,18 +186,8 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
const availableVars = map[node.id].availableVars
for (const variable of usedVars) {
const isSpecialVars = isSpecialVar(variable[0])
if (!isSpecialVars) {
const usedNode = availableVars.find(v => v.nodeId === variable?.[0])
if (usedNode) {
const usedVar = usedNode.vars.find(v => v.variable === variable?.[1])
if (!usedVar)
errorMessage = t('errorMsg.invalidVariable', { ns: 'workflow' })
}
else {
errorMessage = t('errorMsg.invalidVariable', { ns: 'workflow' })
}
}
if (!isValueSelectorInNodeOutputVars(variable, availableVars))
errorMessage = t('errorMsg.invalidVariable', { ns: 'workflow' })
}
}
@@ -383,20 +373,9 @@ export const useChecklistBeforePublish = () => {
const availableVars = map[node.id].availableVars
for (const variable of usedVars) {
const isSpecialVars = isSpecialVar(variable[0])
if (!isSpecialVars) {
const usedNode = availableVars.find(v => v.nodeId === variable?.[0])
if (usedNode) {
const usedVar = usedNode.vars.find(v => v.variable === variable?.[1])
if (!usedVar) {
notify({ type: 'error', message: `[${node.data.title}] ${t('errorMsg.invalidVariable', { ns: 'workflow' })}` })
return false
}
}
else {
notify({ type: 'error', message: `[${node.data.title}] ${t('errorMsg.invalidVariable', { ns: 'workflow' })}` })
return false
}
if (!isValueSelectorInNodeOutputVars(variable, availableVars)) {
notify({ type: 'error', message: `[${node.data.title}] ${t('errorMsg.invalidVariable', { ns: 'workflow' })}` })
return false
}
}

View File

@@ -16,7 +16,7 @@ import {
import { useInvalidateConversationVarValues, useInvalidateSysVarValues } from '@/service/use-workflow'
import { fetchAllInspectVars } from '@/service/workflow'
import useMatchSchemaType from '../nodes/_base/components/variable/use-match-schema-type'
import { toNodeOutputVars } from '../nodes/_base/components/variable/utils'
import { isValueSelectorInNodeOutputVars, toNodeOutputVars } from '../nodes/_base/components/variable/utils'
import { applyAgentSubgraphInspectVars } from './inspect-vars-agent-alias'
type Params = {
@@ -90,10 +90,14 @@ export const useSetWorkflowVarsWithValue = ({
const nodesWithVars: NodeWithVar[] = withValueNodes.map((node) => {
const nodeId = node.id
const isParentNode = resolvedInteractionMode === InteractionMode.Subgraph && parentNodeIds.has(nodeId)
const varsUnderTheNode = inspectVars.filter((varItem) => {
return varItem.selector[0] === nodeId
})
const nodeVar = allNodesOutputVars.find(item => item.nodeId === nodeId)
const varsUnderTheNode = inspectVars.filter((varItem) => {
if (varItem.selector[0] !== nodeId)
return false
if (!nodeVar)
return false
return isValueSelectorInNodeOutputVars(varItem.selector, [nodeVar])
})
return {
nodeId,

View File

@@ -12,6 +12,7 @@ import {
isConversationVar,
isENV,
isSystemVar,
isValueSelectorInNodeOutputVars,
toNodeOutputVars,
} from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { useWorkflowStore } from '@/app/components/workflow/store'
@@ -140,13 +141,15 @@ export const useInspectVarsCrudCommon = ({
}
const currentNodeOutputVars = toNodeOutputVars([currentNode], false, () => true, [], [], [], allPluginInfoList, schemaTypeDefinitions)
const vars = await fetchNodeInspectVars(flowType, flowId, nodeId)
const varsWithSchemaType = vars.map((varItem) => {
const schemaType = currentNodeOutputVars[0]?.vars.find(v => v.variable === varItem.name)?.schemaType || ''
return {
...varItem,
schemaType,
}
})
const varsWithSchemaType = vars
.filter(varItem => isValueSelectorInNodeOutputVars(varItem.selector, currentNodeOutputVars))
.map((varItem) => {
const schemaType = currentNodeOutputVars[0]?.vars.find(v => v.variable === varItem.name)?.schemaType || ''
return {
...varItem,
schemaType,
}
})
setNodeInspectVars(nodeId, varsWithSchemaType)
const resolvedInteractionMode = interactionMode ?? InteractionMode.Default
if (resolvedInteractionMode !== InteractionMode.Subgraph) {
@@ -154,16 +157,29 @@ export const useInspectVarsCrudCommon = ({
const nextNodes = applyAgentSubgraphInspectVars(nodesWithInspectVars, nodeArr)
setNodesWithInspectVars(nextNodes)
}
}, [workflowStore, flowType, flowId, invalidateSysVarValues, invalidateConversationVarValues, buildInTools, customTools, workflowTools, mcpTools, interactionMode])
}, [workflowStore, flowType, flowId, invalidateSysVarValues, invalidateConversationVarValues, buildInTools, customTools, workflowTools, mcpTools, interactionMode, store])
// after last run would call this
const appendNodeInspectVars = useCallback((nodeId: string, payload: VarInInspect[], allNodes: Node[]) => {
const { dataSourceList } = workflowStore.getState()
const nodeInfo = allNodes.find(node => node.id === nodeId)
const allPluginInfoList = {
buildInTools: buildInTools || [],
customTools: customTools || [],
workflowTools: workflowTools || [],
mcpTools: mcpTools || [],
dataSourceList: dataSourceList || [],
}
const currentNodeOutputVars = nodeInfo
? toNodeOutputVars([nodeInfo], false, () => true, [], [], [], allPluginInfoList)
: []
const validPayload = payload.filter(varItem => isValueSelectorInNodeOutputVars(varItem.selector, currentNodeOutputVars))
const {
nodesWithInspectVars,
setNodesWithInspectVars,
} = workflowStore.getState()
const nodes = produce(nodesWithInspectVars, (draft) => {
const nodeInfo = allNodes.find(node => node.id === nodeId)
if (nodeInfo) {
const index = draft.findIndex(node => node.nodeId === nodeId)
if (index === -1) {
@@ -171,12 +187,12 @@ export const useInspectVarsCrudCommon = ({
nodeId,
nodeType: nodeInfo.data.type,
title: nodeInfo.data.title,
vars: payload,
vars: validPayload,
nodePayload: nodeInfo.data,
})
}
else {
draft[index].vars = payload
draft[index].vars = validPayload
// put the node to the topAdd commentMore actions
draft.unshift(draft.splice(index, 1)[0])
}
@@ -187,7 +203,7 @@ export const useInspectVarsCrudCommon = ({
const nextNodes = shouldApplyAlias ? applyAgentSubgraphInspectVars(nodes, allNodes) : nodes
setNodesWithInspectVars(nextNodes)
handleCancelNodeSuccessStatus(nodeId)
}, [workflowStore, handleCancelNodeSuccessStatus, interactionMode])
}, [workflowStore, handleCancelNodeSuccessStatus, interactionMode, buildInTools, customTools, workflowTools, mcpTools])
const hasNodeInspectVar = useCallback((nodeId: string, varId: string) => {
const { nodesWithInspectVars } = workflowStore.getState()

View File

@@ -1,13 +1,14 @@
import type {
CommonNodeType,
Node,
NodeOutPutVar,
ValueSelector,
VarType,
} from '@/app/components/workflow/types'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useNodes, useReactFlow, useStoreApi } from 'reactflow'
import { getNodeInfoById, isConversationVar, isENV, isGlobalVar, isRagVariableVar, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { getNodeInfoById, isConversationVar, isENV, isGlobalVar, isRagVariableVar, isSystemVar, isValueSelectorInNodeOutputVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import {
VariableLabelInSelect,
} from '@/app/components/workflow/nodes/_base/components/variable/variable-label'
@@ -19,12 +20,14 @@ type VariableTagProps = {
varType: VarType
isShort?: boolean
availableNodes?: Node[]
availableVars?: NodeOutPutVar[]
}
const VariableTag = ({
valueSelector,
varType,
isShort,
availableNodes,
availableVars,
}: VariableTagProps) => {
const nodes = useNodes<CommonNodeType>()
const isRagVar = isRagVariableVar(valueSelector)
@@ -40,7 +43,12 @@ const VariableTag = ({
const isEnv = isENV(valueSelector)
const isChatVar = isConversationVar(valueSelector)
const isGlobal = isGlobalVar(valueSelector)
const isValid = Boolean(node) || isEnv || isChatVar || isRagVar || isGlobal
const isValid = useMemo(() => {
if (availableVars)
return isValueSelectorInNodeOutputVars(valueSelector, availableVars)
return Boolean(node) || isEnv || isChatVar || isRagVar || isGlobal
}, [availableVars, valueSelector, node, isEnv, isChatVar, isRagVar, isGlobal])
const variableName = isSystemVar(valueSelector) ? valueSelector.slice(0).join('.') : valueSelector.slice(1).join('.')
const isException = isExceptionVariable(variableName, node?.data.type)

View File

@@ -340,6 +340,29 @@ const findExceptVarInObject = (
return res
}
const getLLMNodeOutputVars = (llmNodeData: LLMNodeType): Var[] => {
const isComputerUseEnabled = !!llmNodeData.computer_use
const vars = [...LLM_OUTPUT_STRUCT].filter((item) => {
if (isComputerUseEnabled)
return true
return item.variable !== 'generation'
})
if (
llmNodeData.structured_output_enabled
&& llmNodeData.structured_output?.schema?.properties
&& Object.keys(llmNodeData.structured_output.schema.properties).length > 0
) {
vars.push({
variable: 'structured_output',
type: VarType.object,
children: llmNodeData.structured_output,
})
}
return vars
}
const formatItem = (
item: any,
isChatMode: boolean,
@@ -415,18 +438,8 @@ const formatItem = (
}
case BlockEnum.LLM: {
res.vars = [...LLM_OUTPUT_STRUCT]
if (
data.structured_output_enabled
&& data.structured_output?.schema?.properties
&& Object.keys(data.structured_output.schema.properties).length > 0
) {
res.vars.push({
variable: 'structured_output',
type: VarType.object,
children: data.structured_output,
})
}
const llmNodeData = data as LLMNodeType
res.vars = getLLMNodeOutputVars(llmNodeData)
break
}
@@ -1304,6 +1317,104 @@ export const getNodeInfoById = (nodes: any, id: string) => {
return nodes.find((node: any) => node.id === id)
}
const normalizeSpecialValueSelector = (valueSelector: ValueSelector): ValueSelector => {
if (valueSelector.length > 1 && isSpecialVar(valueSelector[1]))
return valueSelector.slice(1)
return valueSelector
}
const getVarRootSelector = (nodeId: string, variable: string): ValueSelector => {
const path = variable.split('.')
if (path.length > 0 && isSpecialVar(path[0]))
return path
return [nodeId, ...path]
}
const isSelectorPathValidInStructuredProperties = (
properties: Record<string, StructField> | undefined,
selectorTail: ValueSelector,
): boolean => {
if (!properties)
return false
if (selectorTail.length === 0)
return true
const [currentKey, ...rest] = selectorTail
const property = properties[currentKey]
if (!property)
return false
if (rest.length === 0)
return true
if (property.type === Type.object)
return isSelectorPathValidInStructuredProperties(property.properties, rest)
return false
}
const isSelectorPathValidInVar = (
variable: Var,
selectorTail: ValueSelector,
): boolean => {
if (selectorTail.length === 0)
return true
if (!variable.children)
return false
const structuredProperties = (variable.children as StructuredOutput)?.schema?.properties
if (structuredProperties)
return isSelectorPathValidInStructuredProperties(structuredProperties, selectorTail)
if (!Array.isArray(variable.children))
return false
const [currentKey, ...rest] = selectorTail
const child = variable.children.find(item => item.variable === currentKey)
if (!child)
return false
return isSelectorPathValidInVar(child, rest)
}
const isValueSelectorMatchVar = (
valueSelector: ValueSelector,
nodeId: string,
variable: Var,
): boolean => {
const rootSelector = getVarRootSelector(nodeId, variable.variable)
if (valueSelector.length < rootSelector.length)
return false
const isRootMatched = rootSelector.every((segment, index) => {
return valueSelector[index] === segment
})
if (!isRootMatched)
return false
const selectorTail = valueSelector.slice(rootSelector.length)
return isSelectorPathValidInVar(variable, selectorTail)
}
export const isValueSelectorInNodeOutputVars = (
valueSelector: ValueSelector,
nodeOutputVars: NodeOutPutVar[],
): boolean => {
if (!Array.isArray(valueSelector) || valueSelector.length === 0)
return false
const normalizedSelector = normalizeSpecialValueSelector(valueSelector)
const selectorsToCheck = [valueSelector]
if (normalizedSelector.join('.') !== valueSelector.join('.'))
selectorsToCheck.push(normalizedSelector)
return selectorsToCheck.some((selector) => {
return nodeOutputVars.some((nodeOutputVar) => {
return nodeOutputVar.vars.some((variable) => {
return isValueSelectorMatchVar(selector, nodeOutputVar.nodeId, variable)
})
})
})
}
const matchNotSystemVars = (prompts: string[]) => {
if (!prompts)
return []
@@ -1371,7 +1482,10 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => {
const contextVar = (data as LLMNodeType).context?.variable_selector
? [(data as LLMNodeType).context?.variable_selector]
: []
res = [...inputVars, ...contextVar]
const jinja2VarSelectors = payload.prompt_config?.jinja2_variables
?.map(item => item.value_selector)
.filter(selector => Array.isArray(selector) && selector.length > 0) || []
res = [...inputVars, ...contextVar, ...jinja2VarSelectors]
break
}
case BlockEnum.KnowledgeRetrieval: {
@@ -1416,6 +1530,14 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => {
})
break
}
case BlockEnum.Command: {
const payload = data as CommandNodeType
res = matchNotSystemVars([
payload.command,
payload.working_directory,
])
break
}
case BlockEnum.QuestionClassifier: {
const payload = data as QuestionClassifierNodeType
res = [payload.query_variable_selector]
@@ -2081,19 +2203,8 @@ export const getNodeOutputVars = (
}
case BlockEnum.LLM: {
const vars = [...LLM_OUTPUT_STRUCT]
const llmNodeData = data as LLMNodeType
if (
llmNodeData.structured_output_enabled
&& llmNodeData.structured_output?.schema?.properties
&& Object.keys(llmNodeData.structured_output.schema.properties).length > 0
) {
vars.push({
variable: 'structured_output',
type: VarType.object,
children: llmNodeData.structured_output,
})
}
const vars = getLLMNodeOutputVars(llmNodeData)
varsToValueSelectorList(vars, [id], res)
break
}

View File

@@ -49,7 +49,7 @@ import { cn } from '@/utils/classnames'
import useAvailableVarList from '../../hooks/use-available-var-list'
import RemoveButton from '../remove-button'
import ConstantField from './constant-field'
import { getNodeInfoById, isConversationVar, isENV, isGlobalVar, isRagVariableVar, isSystemVar, removeFileVars, varTypeToStructType } from './utils'
import { getNodeInfoById, isConversationVar, isENV, isGlobalVar, isRagVariableVar, isSystemVar, isValueSelectorInNodeOutputVars, removeFileVars, varTypeToStructType } from './utils'
import VarFullPathPanel from './var-full-path-panel'
import VarReferencePopup from './var-reference-popup'
@@ -312,7 +312,9 @@ const VarReferencePicker: FC<Props> = ({
const isChatVar = isConversationVar(value as ValueSelector)
const isGlobal = isGlobalVar(value as ValueSelector)
const isRagVar = isRagVariableVar(value as ValueSelector)
const isValidVar = Boolean(outputVarNode) || isEnv || isChatVar || isGlobal || isRagVar
const isValidVar = !hasValue || !Array.isArray(value)
? true
: isValueSelectorInNodeOutputVars(value, outputVars)
const isException = isExceptionVariable(varName, outputVarNode?.type)
return {
isEnv,
@@ -322,7 +324,7 @@ const VarReferencePicker: FC<Props> = ({
isValidVar,
isException,
}
}, [value, outputVarNode, varName])
}, [value, hasValue, outputVarNode, outputVars, varName])
// 8(left/right-padding) + 14(icon) + 4 + 14 + 2 = 42 + 17 buff
const availableWidth = triggerWidth - 56

View File

@@ -27,6 +27,77 @@ import { Type } from '../../../llm/types'
import ManageInputField from './manage-input-field'
import { isSpecialVar, varTypeToStructType } from './utils'
const isStructuredOutputChildren = (children?: Var['children']): children is StructuredOutput => {
return !!(children as StructuredOutput | undefined)?.schema?.properties
}
const matchesPath = (segments: string[], query: string) => {
return segments.join('.').toLowerCase().includes(query)
}
const matchesStructuredProperties = (
properties: Record<string, Field>,
query: string,
prefix: string[],
): boolean => {
return Object.keys(properties).some((key) => {
const field = properties[key]
const nextPath = [...prefix, key]
if (matchesPath(nextPath, query))
return true
if (field.type === Type.object && field.properties)
return matchesStructuredProperties(field.properties, query, nextPath)
if (field.type === Type.array && field.items?.type === Type.object && field.items?.properties)
return matchesStructuredProperties(field.items.properties, query, nextPath)
return false
})
}
const matchesStructuredOutput = (
structuredOutput: StructuredOutput,
query: string,
prefix: string[],
): boolean => {
return matchesStructuredProperties(structuredOutput.schema.properties, query, prefix)
}
const matchesVarChildren = (children: Var[], query: string, prefix: string[]): boolean => {
return children.some((child) => {
const nextPath = [...prefix, child.variable]
if (matchesPath(nextPath, query))
return true
const childChildren = child.children
if (!childChildren)
return false
if (Array.isArray(childChildren))
return matchesVarChildren(childChildren, query, nextPath)
if (isStructuredOutputChildren(childChildren))
return matchesStructuredOutput(childChildren, query, nextPath)
return false
})
}
const matchesNestedVar = (itemData: Var, query: string): boolean => {
const children = itemData.children
if (!children)
return false
if (Array.isArray(children))
return matchesVarChildren(children, query, [itemData.variable])
if (isStructuredOutputChildren(children))
return matchesStructuredOutput(children, query, [itemData.variable])
return false
}
type ItemProps = {
nodeId: string
title: string
@@ -362,7 +433,11 @@ const VarReferenceVars: FC<Props> = ({
const matchedByTitle = titleLower.includes(normalizedSearchTextLower)
const nodeVars = matchedByTitle
? node.vars
: node.vars.filter(v => v.variable.toLowerCase().includes(normalizedSearchTextLower))
: node.vars.filter((v) => {
if (v.variable.toLowerCase().includes(normalizedSearchTextLower))
return true
return matchesNestedVar(v, normalizedSearchTextLower)
})
if (nodeVars.length === 0)
return
res.push({

View File

@@ -54,8 +54,10 @@ export const useVarColor = (variables: string[], isExceptionVariable?: boolean,
}
export const useVarName = (variables: string[], notShowFullPath?: boolean) => {
const showName = VAR_SHOW_NAME_MAP[variables.join('.')]
let variableFullPathName = variables.slice(1).join('.')
const fullPathKey = variables.join('.')
const keyWithoutNodePrefix = variables.slice(1).join('.')
const showName = VAR_SHOW_NAME_MAP[fullPathKey] ?? VAR_SHOW_NAME_MAP[keyWithoutNodePrefix]
let variableFullPathName = keyWithoutNodePrefix
if (isRagVariableVar(variables))
variableFullPathName = variables.slice(2).join('.')

View File

@@ -38,6 +38,7 @@ const ConditionVarSelector = ({
valueSelector={valueSelector}
varType={varType}
availableNodes={availableNodes}
availableVars={nodesOutputVars}
isShort
/>
</div>

View File

@@ -121,6 +121,7 @@ const ConditionNumberInput = ({
<VariableTag
valueSelector={variableTransformer(value) as string[]}
varType={VarType.number}
availableVars={variables}
isShort={isShort}
/>
)

View File

@@ -57,6 +57,7 @@ const ConditionVariableSelector = ({
valueSelector={valueSelector}
varType={varType}
availableNodes={availableNodes}
availableVars={nodesOutputVars}
isShort
/>
)

View File

@@ -75,7 +75,7 @@ const ConfigContextItem: FC<Props> = ({
)}
>
<div className="text-text-secondary system-xs-semibold-uppercase">
{t('nodes.llm.context', { ns: 'workflow' })}
{t('nodes.llm.chatHistorry', { ns: 'workflow' })}
</div>
<div className="flex items-center gap-1">
<VariableLabelInSelect

View File

@@ -1,40 +0,0 @@
import type { FC } from 'react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Switch from '@/app/components/base/switch'
import Field from '@/app/components/workflow/nodes/_base/components/field'
type ReasoningFormatConfigProps = {
value?: 'tagged' | 'separated'
onChange: (value: 'tagged' | 'separated') => void
readonly?: boolean
}
const ReasoningFormatConfig: FC<ReasoningFormatConfigProps> = ({
value = 'tagged',
onChange,
readonly = false,
}) => {
const { t } = useTranslation()
return (
<Field
title={t('nodes.llm.reasoningFormat.title', { ns: 'workflow' })}
tooltip={t('nodes.llm.reasoningFormat.tooltip', { ns: 'workflow' })}
operations={(
// ON = separated, OFF = tagged
<Switch
defaultValue={value === 'separated'}
onChange={enabled => onChange(enabled ? 'separated' : 'tagged')}
size="md"
disabled={readonly}
key={value}
/>
)}
>
<div />
</Field>
)
}
export default ReasoningFormatConfig

View File

@@ -23,7 +23,6 @@ import MemoryConfig from '../_base/components/memory-config'
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
import ComputerUseConfig from './components/computer-use-config'
import ConfigPrompt from './components/config-prompt'
import ReasoningFormatConfig from './components/reasoning-format-config'
import StructureOutput from './components/structure-output'
import Tools from './components/tools'
import MaxIterations from './components/tools/max-iterations'
@@ -72,7 +71,6 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
handleStructureOutputEnableChange,
handleStructureOutputChange,
filterJinja2InputVar,
handleReasoningFormatChange,
isSupportSandbox,
handleComputerUseChange,
} = useConfig(id, data)
@@ -335,13 +333,6 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
value={inputs.max_iterations}
onChange={handleMaxIterationsChange}
/>
{/* Reasoning Format */}
<ReasoningFormatConfig
value={inputs.reasoning_format || 'tagged'}
onChange={handleReasoningFormatChange}
readonly={readOnly}
/>
</div>
</FieldCollapse>
@@ -410,28 +401,30 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
)}
>
<>
<VarItem
name="generation"
type="object"
description={t(`${i18nPrefix}.outputVars.generation`, { ns: 'workflow' })}
subItems={[
{
name: 'content',
type: 'string',
description: '',
},
{
name: 'reasoning_content',
type: 'array[string]',
description: '',
},
{
name: 'tool_calls',
type: 'array[object]',
description: '',
},
]}
/>
{!!inputs.computer_use && (
<VarItem
name="generation"
type="object"
description={t(`${i18nPrefix}.outputVars.generation`, { ns: 'workflow' })}
subItems={[
{
name: 'content',
type: 'string',
description: '',
},
{
name: 'reasoning_content',
type: 'array[string]',
description: '',
},
{
name: 'tool_calls',
type: 'array[object]',
description: '',
},
]}
/>
)}
<VarItem
name="text"
type="string"

View File

@@ -38,7 +38,6 @@ export type LLMNodeType = CommonNodeType & {
}
structured_output_enabled?: boolean
structured_output?: StructuredOutput
reasoning_format?: 'tagged' | 'separated'
tools?: ToolValue[]
tool_settings?: ToolSetting[]
max_iterations?: number

View File

@@ -362,14 +362,6 @@ const useConfig = (id: string, payload: LLMNodeType) => {
return [VarType.arrayObject, VarType.array, VarType.number, VarType.string, VarType.secret, VarType.arrayString, VarType.arrayNumber, VarType.file, VarType.arrayFile].includes(varPayload.type)
}, [])
// reasoning format
const handleReasoningFormatChange = useCallback((reasoningFormat: 'tagged' | 'separated') => {
const newInputs = produce(inputRef.current, (draft) => {
draft.reasoning_format = reasoningFormat
})
setInputs(newInputs)
}, [setInputs])
const {
availableVars,
availableNodesWithParent,
@@ -411,7 +403,6 @@ const useConfig = (id: string, payload: LLMNodeType) => {
setStructuredOutputCollapsed,
handleStructureOutputEnableChange,
filterJinja2InputVar,
handleReasoningFormatChange,
isSupportSandbox,
handleComputerUseChange,
}

View File

@@ -38,6 +38,7 @@ const ConditionVarSelector = ({
valueSelector={valueSelector}
varType={varType}
availableNodes={availableNodes}
availableVars={nodesOutputVars}
isShort
/>
</div>

View File

@@ -121,6 +121,7 @@ const ConditionNumberInput = ({
<VariableTag
valueSelector={variableTransformer(value) as string[]}
varType={VarType.number}
availableVars={variables}
isShort={isShort}
/>
)

View File

@@ -1482,7 +1482,7 @@
},
"app/components/base/prompt-editor/plugins/hitl-input-block/index.tsx": {
"react-refresh/only-export-components": {
"count": 3
"count": 2
}
},
"app/components/base/prompt-editor/plugins/last-run-block/index.tsx": {

View File

@@ -780,10 +780,6 @@
"nodes.llm.outputVars.reasoning_content": "محتوى التفكير",
"nodes.llm.outputVars.usage": "معلومات استخدام النموذج",
"nodes.llm.prompt": "المطالبة",
"nodes.llm.reasoningFormat.separated": "فصل علامات التفكير",
"nodes.llm.reasoningFormat.tagged": "الاحتفاظ بعلامات التفكير",
"nodes.llm.reasoningFormat.title": "تمكين فصل علامة التفكير",
"nodes.llm.reasoningFormat.tooltip": "استخراج المحتوى من علامات التفكير وتخزينه في حقل content_reasoning.",
"nodes.llm.resolution.high": "عالية",
"nodes.llm.resolution.low": "منخفضة",
"nodes.llm.resolution.name": "الدقة",

View File

@@ -780,10 +780,6 @@
"nodes.llm.outputVars.reasoning_content": "Reasoning-Inhalt",
"nodes.llm.outputVars.usage": "Nutzungsinformationen des Modells",
"nodes.llm.prompt": "Prompt",
"nodes.llm.reasoningFormat.separated": "Separate Denk tags",
"nodes.llm.reasoningFormat.tagged": "Behalte die Denk-Tags",
"nodes.llm.reasoningFormat.title": "Aktivieren Sie die Trennung von Argumentations-Tags",
"nodes.llm.reasoningFormat.tooltip": "Inhalte aus Denk-Tags extrahieren und im Feld reasoning_content speichern.",
"nodes.llm.resolution.high": "Hoch",
"nodes.llm.resolution.low": "Niedrig",
"nodes.llm.resolution.name": "Auflösung",

View File

@@ -210,7 +210,7 @@
"showMyCreatedAppsOnly": "Created by me",
"structOutput.LLMResponse": "LLM Response",
"structOutput.configure": "Configure",
"structOutput.disabledByComputerUse": "Structured Output is disabled when Computer Use is enabled.",
"structOutput.disabledByComputerUse": "Structured Output is disabled when Agent Mode is enabled.",
"structOutput.disabledByTools": "Structured Output is disabled when tools are enabled.",
"structOutput.modelNotSupported": "Model not supported",
"structOutput.modelNotSupportedTip": "The current model does not support this feature and is automatically downgraded to prompt injection.",

View File

@@ -497,8 +497,8 @@
"plugin.serpapi.apiKeyPlaceholder": "Enter your API key",
"plugin.serpapi.keyFrom": "Get your SerpAPI key from SerpAPI Account Page",
"promptEditor.context.item.desc": "Insert context template",
"promptEditor.context.item.title": "Chat History",
"promptEditor.context.modal.add": "Add Chat History ",
"promptEditor.context.item.title": "Context",
"promptEditor.context.modal.add": "Add Context ",
"promptEditor.context.modal.footer": "You can manage contexts in the Context section below.",
"promptEditor.context.modal.title": "{{num}} Knowledge in Context",
"promptEditor.existed": "Already exists in the prompt",
@@ -616,8 +616,8 @@
"sandboxProvider.notConfigured": "Not Configured",
"sandboxProvider.otherProvider": "OTHER PROVIDERS",
"sandboxProvider.setAsActive": "Set as Active",
"sandboxProvider.ssh.description": "Run agent workloads in a remote SSH VM with file transfer support.",
"sandboxProvider.ssh.label": "SSH VM",
"sandboxProvider.ssh.description": "Run agent workloads in a remote SSH with file transfer support.",
"sandboxProvider.ssh.label": "SSH",
"sandboxProvider.switchModal.cancel": "Cancel",
"sandboxProvider.switchModal.confirm": "Switch",
"sandboxProvider.switchModal.confirmText": "You are about to switch the active sandbox provider to <bold>{{provider}}</bold>.",

View File

@@ -776,6 +776,7 @@
"nodes.llm.addContext": "Add Context",
"nodes.llm.addMessage": "Add Message",
"nodes.llm.advancedSettings": "Advanced Settings",
"nodes.llm.chatHistorry": "Chat History",
"nodes.llm.computerUse.disabledByStructuredOutput": "Agent mode is disabled when Structured Output is enabled.",
"nodes.llm.computerUse.referenceTools": "Reference Tools",
"nodes.llm.computerUse.referenceToolsEmpty": "Tools referenced in the prompt will appear here",
@@ -818,10 +819,6 @@
"nodes.llm.outputVars.reasoning_content": "Reasoning Content",
"nodes.llm.outputVars.usage": "Model Usage Information",
"nodes.llm.prompt": "prompt",
"nodes.llm.reasoningFormat.separated": "Separate think tags",
"nodes.llm.reasoningFormat.tagged": "Keep think tags",
"nodes.llm.reasoningFormat.title": "Enable reasoning tag separation",
"nodes.llm.reasoningFormat.tooltip": "Extract content from think tags and store it in the reasoning_content field.",
"nodes.llm.removeContext": "Remove context",
"nodes.llm.resolution.high": "High",
"nodes.llm.resolution.low": "Low",

View File

@@ -780,10 +780,6 @@
"nodes.llm.outputVars.reasoning_content": "Contenido de razonamiento",
"nodes.llm.outputVars.usage": "Información de uso del modelo",
"nodes.llm.prompt": "indicación",
"nodes.llm.reasoningFormat.separated": "Separar etiquetas de pensamiento",
"nodes.llm.reasoningFormat.tagged": "Mantén las etiquetas de pensamiento",
"nodes.llm.reasoningFormat.title": "Habilitar la separación de etiquetas de razonamiento",
"nodes.llm.reasoningFormat.tooltip": "Extraer contenido de las etiquetas de pensamiento y almacenarlo en el campo reasoning_content.",
"nodes.llm.resolution.high": "Alta",
"nodes.llm.resolution.low": "Baja",
"nodes.llm.resolution.name": "Resolución",

View File

@@ -780,10 +780,6 @@
"nodes.llm.outputVars.reasoning_content": "محتوای استدلال",
"nodes.llm.outputVars.usage": "اطلاعات استفاده از مدل",
"nodes.llm.prompt": "پیشنهاد",
"nodes.llm.reasoningFormat.separated": "تگ‌های تفکر جداگانه",
"nodes.llm.reasoningFormat.tagged": "به فکر برچسب‌ها باشید",
"nodes.llm.reasoningFormat.title": "فعال‌سازی جداسازی برچسب‌های استدلال",
"nodes.llm.reasoningFormat.tooltip": "محتوا را از تگ‌های تفکر استخراج کرده و در فیلد reasoning_content ذخیره کنید.",
"nodes.llm.resolution.high": "بالا",
"nodes.llm.resolution.low": "پایین",
"nodes.llm.resolution.name": "وضوح",

View File

@@ -780,10 +780,6 @@
"nodes.llm.outputVars.reasoning_content": "Contenu de raisonnement",
"nodes.llm.outputVars.usage": "Informations sur l'utilisation du modèle",
"nodes.llm.prompt": "invite",
"nodes.llm.reasoningFormat.separated": "Séparer les balises de réflexion",
"nodes.llm.reasoningFormat.tagged": "Gardez les étiquettes de pensée",
"nodes.llm.reasoningFormat.title": "Activer la séparation des balises de raisonnement",
"nodes.llm.reasoningFormat.tooltip": "Extraire le contenu des balises think et le stocker dans le champ reasoning_content.",
"nodes.llm.resolution.high": "Haute",
"nodes.llm.resolution.low": "Basse",
"nodes.llm.resolution.name": "Résolution",

View File

@@ -780,10 +780,6 @@
"nodes.llm.outputVars.reasoning_content": "तर्क सामग्री",
"nodes.llm.outputVars.usage": "मॉडल उपयोग जानकारी",
"nodes.llm.prompt": "प्रॉम्प्ट",
"nodes.llm.reasoningFormat.separated": "अलग सोच टैग",
"nodes.llm.reasoningFormat.tagged": "टैग्स के बारे में सोचते रहें",
"nodes.llm.reasoningFormat.title": "कारण संबंध टैग विभाजन सक्षम करें",
"nodes.llm.reasoningFormat.tooltip": "थिंक टैग से सामग्री निकाले और इसे reasoning_content क्षेत्र में संग्रहित करें।",
"nodes.llm.resolution.high": "उच्च",
"nodes.llm.resolution.low": "निम्न",
"nodes.llm.resolution.name": "रेजोल्यूशन",

View File

@@ -780,10 +780,6 @@
"nodes.llm.outputVars.reasoning_content": "Konten penalaran",
"nodes.llm.outputVars.usage": "Informasi Penggunaan Model",
"nodes.llm.prompt": "cepat",
"nodes.llm.reasoningFormat.separated": "Pisahkan tag pemikiran",
"nodes.llm.reasoningFormat.tagged": "Tetap pikirkan tag",
"nodes.llm.reasoningFormat.title": "Aktifkan pemisahan tag penalaran",
"nodes.llm.reasoningFormat.tooltip": "Ekstrak konten dari tag pikir dan simpan di field reasoning_content.",
"nodes.llm.resolution.high": "Tinggi",
"nodes.llm.resolution.low": "Rendah",
"nodes.llm.resolution.name": "Resolusi",

View File

@@ -780,10 +780,6 @@
"nodes.llm.outputVars.reasoning_content": "Contenuto del ragionamento",
"nodes.llm.outputVars.usage": "Informazioni sull'utilizzo del modello",
"nodes.llm.prompt": "prompt",
"nodes.llm.reasoningFormat.separated": "Tag di pensiero separati",
"nodes.llm.reasoningFormat.tagged": "Continua a pensare ai tag",
"nodes.llm.reasoningFormat.title": "Abilita la separazione dei tag di ragionamento",
"nodes.llm.reasoningFormat.tooltip": "Estrai il contenuto dai tag think e conservalo nel campo reasoning_content.",
"nodes.llm.resolution.high": "Alta",
"nodes.llm.resolution.low": "Bassa",
"nodes.llm.resolution.name": "Risoluzione",

View File

@@ -207,7 +207,7 @@
"showMyCreatedAppsOnly": "自分が作成したアプリ",
"structOutput.LLMResponse": "LLM のレスポンス",
"structOutput.configure": "設定",
"structOutput.disabledByComputerUse": "Computer Use を有効にすると、構造化出力は無効になります。",
"structOutput.disabledByComputerUse": "Agent Mode を有効にすると、構造化出力は無効になります。",
"structOutput.disabledByTools": "ツールを有効にすると、構造化出力は無効になります。",
"structOutput.modelNotSupported": "モデルが対応していません",
"structOutput.modelNotSupportedTip": "現在のモデルはこの機能に対応しておらず、自動的にプロンプトインジェクションに切り替わります。",

View File

@@ -497,8 +497,8 @@
"plugin.serpapi.apiKeyPlaceholder": "API キーを入力してください",
"plugin.serpapi.keyFrom": "SerpAPI アカウントページから SerpAPI キーを取得してください",
"promptEditor.context.item.desc": "コンテキストテンプレートを挿入",
"promptEditor.context.item.title": "チャット履歴",
"promptEditor.context.modal.add": "チャット履歴を追加",
"promptEditor.context.item.title": "コンテキスト",
"promptEditor.context.modal.add": "コンテキストを追加",
"promptEditor.context.modal.footer": "以下のコンテキストセクションでコンテキストを管理できます。",
"promptEditor.context.modal.title": "{{num}} 番目のコンテキスト",
"promptEditor.existed": "プロンプトにすでに存在します",

View File

@@ -751,6 +751,7 @@
"nodes.listFilter.selectVariableKeyPlaceholder": "サブ変数キーを選択する",
"nodes.llm.addContext": "コンテキスト追加",
"nodes.llm.addMessage": "メッセージ追加",
"nodes.llm.chatHistorry": "チャット履歴",
"nodes.llm.computerUse.disabledByStructuredOutput": "構造化出力を有効にすると、Agent mode は無効になります。",
"nodes.llm.computerUse.title": "Agent mode",
"nodes.llm.computerUse.tooltip": "Agent mode で実行時ファイルシステムとツールアクセスを管理します。",
@@ -790,10 +791,6 @@
"nodes.llm.outputVars.reasoning_content": "推論内容",
"nodes.llm.outputVars.usage": "モデル使用量",
"nodes.llm.prompt": "プロンプト",
"nodes.llm.reasoningFormat.separated": "思考タグを分ける",
"nodes.llm.reasoningFormat.tagged": "タグを考え続けてください",
"nodes.llm.reasoningFormat.title": "推論タグの分離を有効にする",
"nodes.llm.reasoningFormat.tooltip": "thinkタグから内容を抽出し、それをreasoning_contentフィールドに保存します。",
"nodes.llm.removeContext": "コンテキストを削除",
"nodes.llm.resolution.high": "高",
"nodes.llm.resolution.low": "低",

View File

@@ -780,10 +780,6 @@
"nodes.llm.outputVars.reasoning_content": "추론 내용",
"nodes.llm.outputVars.usage": "모델 사용 정보",
"nodes.llm.prompt": "프롬프트",
"nodes.llm.reasoningFormat.separated": "추론 태그 분리",
"nodes.llm.reasoningFormat.tagged": "추론 태그 유지",
"nodes.llm.reasoningFormat.title": "추론 태그 분리 활성화",
"nodes.llm.reasoningFormat.tooltip": "추론 태그에서 내용을 추출하고 이를 reasoning_content 필드에 저장합니다",
"nodes.llm.resolution.high": "높음",
"nodes.llm.resolution.low": "낮음",
"nodes.llm.resolution.name": "해상도",

View File

@@ -780,10 +780,6 @@
"nodes.llm.outputVars.reasoning_content": "Treść rozumowania",
"nodes.llm.outputVars.usage": "Informacje o użyciu modelu",
"nodes.llm.prompt": "prompt",
"nodes.llm.reasoningFormat.separated": "Oddziel tagi myślenia",
"nodes.llm.reasoningFormat.tagged": "Zachowaj myśl tagi",
"nodes.llm.reasoningFormat.title": "Włącz separację tagów uzasadnienia",
"nodes.llm.reasoningFormat.tooltip": "Wyodrębnij treść z tagów think i przechowaj ją w polu reasoning_content.",
"nodes.llm.resolution.high": "Wysoka",
"nodes.llm.resolution.low": "Niska",
"nodes.llm.resolution.name": "Rozdzielczość",

View File

@@ -780,10 +780,6 @@
"nodes.llm.outputVars.reasoning_content": "Conteúdo de raciocínio",
"nodes.llm.outputVars.usage": "Informações de uso do modelo",
"nodes.llm.prompt": "prompt",
"nodes.llm.reasoningFormat.separated": "Separe as tags de pensamento",
"nodes.llm.reasoningFormat.tagged": "Mantenha as tags de pensamento",
"nodes.llm.reasoningFormat.title": "Ativar separação de tags de raciocínio",
"nodes.llm.reasoningFormat.tooltip": "Extraia o conteúdo das tags de pensamento e armazene-o no campo reasoning_content.",
"nodes.llm.resolution.high": "Alta",
"nodes.llm.resolution.low": "Baixa",
"nodes.llm.resolution.name": "Resolução",

View File

@@ -780,10 +780,6 @@
"nodes.llm.outputVars.reasoning_content": "Conținut de raționament",
"nodes.llm.outputVars.usage": "Informații de utilizare a modelului",
"nodes.llm.prompt": "prompt",
"nodes.llm.reasoningFormat.separated": "Etichete de gândire separate",
"nodes.llm.reasoningFormat.tagged": "Ține minte etichetele",
"nodes.llm.reasoningFormat.title": "Activează separarea etichetelor de raționare",
"nodes.llm.reasoningFormat.tooltip": "Extrage conținutul din etichetele think și stochează-l în câmpul reasoning_content.",
"nodes.llm.resolution.high": "Înaltă",
"nodes.llm.resolution.low": "Joasă",
"nodes.llm.resolution.name": "Rezoluție",

View File

@@ -780,10 +780,6 @@
"nodes.llm.outputVars.reasoning_content": "Содержимое рассуждений",
"nodes.llm.outputVars.usage": "Информация об использовании модели",
"nodes.llm.prompt": "подсказка",
"nodes.llm.reasoningFormat.separated": "Отдельные теги для мышления",
"nodes.llm.reasoningFormat.tagged": "Продолжайте думать о тегах",
"nodes.llm.reasoningFormat.title": "Включите разделение тегов на основе логики",
"nodes.llm.reasoningFormat.tooltip": "Извлечь содержимое из тегов think и сохранить его в поле reasoning_content.",
"nodes.llm.resolution.high": "Высокое",
"nodes.llm.resolution.low": "Низкое",
"nodes.llm.resolution.name": "Разрешение",

View File

@@ -780,10 +780,6 @@
"nodes.llm.outputVars.reasoning_content": "Vsebina razmišljanja",
"nodes.llm.outputVars.usage": "Informacije o uporabi modela",
"nodes.llm.prompt": "ukaz",
"nodes.llm.reasoningFormat.separated": "Ločite oznake za razmišljanje",
"nodes.llm.reasoningFormat.tagged": "Ohranite oznake za razmišljanje",
"nodes.llm.reasoningFormat.title": "Omogoči ločevanje oznak za razsojanje",
"nodes.llm.reasoningFormat.tooltip": "Izvleći vsebino iz miselnih oznak in jo shraniti v polje reasoning_content.",
"nodes.llm.resolution.high": "Visoko",
"nodes.llm.resolution.low": "Nizko",
"nodes.llm.resolution.name": "Resolucija",

View File

@@ -780,10 +780,6 @@
"nodes.llm.outputVars.reasoning_content": "เนื้อหาการให้เหตุผล",
"nodes.llm.outputVars.usage": "ข้อมูลการใช้งานรุ่น",
"nodes.llm.prompt": "พร้อมท์",
"nodes.llm.reasoningFormat.separated": "แยกแท็กความคิดเห็น",
"nodes.llm.reasoningFormat.tagged": "รักษาความคิดเกี่ยวกับแท็ก",
"nodes.llm.reasoningFormat.title": "เปิดใช้งานการแยกแท็กการเหตุผล",
"nodes.llm.reasoningFormat.tooltip": "ดึงเนื้อหาจากแท็กคิดและเก็บไว้ในฟิลด์ reasoning_content.",
"nodes.llm.resolution.high": "สูง",
"nodes.llm.resolution.low": "ต่ํา",
"nodes.llm.resolution.name": "มติ",

View File

@@ -780,10 +780,6 @@
"nodes.llm.outputVars.reasoning_content": "Akıl yürütme içeriği",
"nodes.llm.outputVars.usage": "Model Kullanım Bilgileri",
"nodes.llm.prompt": "prompt",
"nodes.llm.reasoningFormat.separated": "Ayrı düşünce etiketleri",
"nodes.llm.reasoningFormat.tagged": "Etiketleri düşünmeye devam et",
"nodes.llm.reasoningFormat.title": "Akıl yürütme etiket ayrımını etkinleştir",
"nodes.llm.reasoningFormat.tooltip": "Düşünce etiketlerinden içeriği çıkarın ve bunu reasoning_content alanında saklayın.",
"nodes.llm.resolution.high": "Yüksek",
"nodes.llm.resolution.low": "Düşük",
"nodes.llm.resolution.name": "Çözünürlük",

View File

@@ -780,10 +780,6 @@
"nodes.llm.outputVars.reasoning_content": "Зміст міркування",
"nodes.llm.outputVars.usage": "Інформація про використання моделі",
"nodes.llm.prompt": "prompt",
"nodes.llm.reasoningFormat.separated": "Окремі теги для думок",
"nodes.llm.reasoningFormat.tagged": "Продовжуйте думати про мітки",
"nodes.llm.reasoningFormat.title": "Увімкніть розділення тегів для міркування",
"nodes.llm.reasoningFormat.tooltip": "Витягніть вміст з тегів think і зберігайте його в полі reasoning_content.",
"nodes.llm.resolution.high": "Висока",
"nodes.llm.resolution.low": "Низька",
"nodes.llm.resolution.name": "Роздільна здатність",

View File

@@ -780,10 +780,6 @@
"nodes.llm.outputVars.reasoning_content": "Nội dung lập luận",
"nodes.llm.outputVars.usage": "Thông tin sử dụng mô hình",
"nodes.llm.prompt": "prompt",
"nodes.llm.reasoningFormat.separated": "Tách biệt các thẻ suy nghĩ",
"nodes.llm.reasoningFormat.tagged": "Giữ lại thẻ suy nghĩ",
"nodes.llm.reasoningFormat.title": "Bật chế độ phân tách nhãn lý luận",
"nodes.llm.reasoningFormat.tooltip": "Trích xuất nội dung từ các thẻ think và lưu nó vào trường reasoning_content.",
"nodes.llm.resolution.high": "Cao",
"nodes.llm.resolution.low": "Thấp",
"nodes.llm.resolution.name": "Độ phân giải",

View File

@@ -208,7 +208,7 @@
"showMyCreatedAppsOnly": "我创建的",
"structOutput.LLMResponse": "LLM 的响应",
"structOutput.configure": "配置",
"structOutput.disabledByComputerUse": "启用计算机使用时,将禁用结构化输出。",
"structOutput.disabledByComputerUse": "启用 Agent 模式时,将禁用结构化输出。",
"structOutput.disabledByTools": "启用工具时,将禁用结构化输出。",
"structOutput.modelNotSupported": "模型不支持",
"structOutput.modelNotSupportedTip": "当前模型不支持此功能,将自动降级为提示注入。",

View File

@@ -497,8 +497,8 @@
"plugin.serpapi.apiKeyPlaceholder": "输入你的 API 密钥",
"plugin.serpapi.keyFrom": "从 SerpAPI 帐户页面获取您的 SerpAPI 密钥",
"promptEditor.context.item.desc": "插入上下文模板",
"promptEditor.context.item.title": "对话历史",
"promptEditor.context.modal.add": "添加对话历史",
"promptEditor.context.item.title": "上下文",
"promptEditor.context.modal.add": "添加上下文",
"promptEditor.context.modal.footer": "您可以在下面的“上下文”部分中管理上下文。",
"promptEditor.context.modal.title": "有 {{num}} 个知识库在上下文中",
"promptEditor.existed": "Prompt 中已存在",

View File

@@ -769,6 +769,7 @@
"nodes.listFilter.selectVariableKeyPlaceholder": "选择子变量的 Key",
"nodes.llm.addContext": "添加上下文",
"nodes.llm.addMessage": "添加消息",
"nodes.llm.chatHistorry": "对话历史",
"nodes.llm.computerUse.disabledByStructuredOutput": "启用结构化输出时,将禁用 Agent 模式。",
"nodes.llm.computerUse.referenceTools": "引用工具",
"nodes.llm.computerUse.referenceToolsEmpty": "提示词中引用的工具会显示在这里",
@@ -811,10 +812,6 @@
"nodes.llm.outputVars.reasoning_content": "推理内容",
"nodes.llm.outputVars.usage": "模型用量信息",
"nodes.llm.prompt": "提示词",
"nodes.llm.reasoningFormat.separated": "分开思考标签",
"nodes.llm.reasoningFormat.tagged": "保持思考标签",
"nodes.llm.reasoningFormat.title": "启用推理标签分离",
"nodes.llm.reasoningFormat.tooltip": "从think标签中提取内容并将其存储在reasoning_content字段中。",
"nodes.llm.removeContext": "删除上下文",
"nodes.llm.resolution.high": "高",
"nodes.llm.resolution.low": "低",

View File

@@ -201,7 +201,7 @@
"showMyCreatedAppsOnly": "我建立的",
"structOutput.LLMResponse": "LLM 回應",
"structOutput.configure": "配置",
"structOutput.disabledByComputerUse": "啟用電腦使用時,將停用結構化輸出。",
"structOutput.disabledByComputerUse": "啟用 Agent 模式時,將停用結構化輸出。",
"structOutput.disabledByTools": "啟用工具時,將停用結構化輸出。",
"structOutput.modelNotSupported": "模型不支持",
"structOutput.modelNotSupportedTip": "當前模型不支持此功能,並自動降級為提示注入。",

View File

@@ -497,8 +497,8 @@
"plugin.serpapi.apiKeyPlaceholder": "輸入你的 API 金鑰",
"plugin.serpapi.keyFrom": "從 SerpAPI 帳戶頁面獲取您的 SerpAPI 金鑰",
"promptEditor.context.item.desc": "插入上下文模板",
"promptEditor.context.item.title": "對話歷史",
"promptEditor.context.modal.add": "新增對話歷史",
"promptEditor.context.item.title": "上下文",
"promptEditor.context.modal.add": "新增上下文",
"promptEditor.context.modal.footer": "您可以在下面的“上下文”部分中管理上下文。",
"promptEditor.context.modal.title": "有 {{num}} 個知識庫在上下文中",
"promptEditor.existed": "Prompt 中已存在",

View File

@@ -751,6 +751,7 @@
"nodes.listFilter.selectVariableKeyPlaceholder": "Select sub variable key選擇子變數鍵",
"nodes.llm.addContext": "新增上下文",
"nodes.llm.addMessage": "新增消息",
"nodes.llm.chatHistorry": "對話歷史",
"nodes.llm.computerUse.disabledByStructuredOutput": "啟用結構化輸出時,將停用 Agent 模式。",
"nodes.llm.computerUse.referenceToolsEmpty": "提示詞中引用的工具會顯示在這裡",
"nodes.llm.computerUse.title": "Agent 模式",
@@ -791,10 +792,6 @@
"nodes.llm.outputVars.reasoning_content": "推理內容",
"nodes.llm.outputVars.usage": "模型用量信息",
"nodes.llm.prompt": "提示詞",
"nodes.llm.reasoningFormat.separated": "分開思考標籤",
"nodes.llm.reasoningFormat.tagged": "保持思考標籤",
"nodes.llm.reasoningFormat.title": "啟用推理標籤分離",
"nodes.llm.reasoningFormat.tooltip": "從 think 標籤中提取內容並將其存儲在 reasoning_content 欄位中。",
"nodes.llm.removeContext": "刪除上下文",
"nodes.llm.resolution.high": "高",
"nodes.llm.resolution.low": "低",