diff --git a/api/bin/dify-cli-darwin-amd64 b/api/bin/dify-cli-darwin-amd64 index 74120973e6..f4cc004d33 100755 Binary files a/api/bin/dify-cli-darwin-amd64 and b/api/bin/dify-cli-darwin-amd64 differ diff --git a/api/bin/dify-cli-darwin-arm64 b/api/bin/dify-cli-darwin-arm64 index 71ff10f725..da8c918e27 100755 Binary files a/api/bin/dify-cli-darwin-arm64 and b/api/bin/dify-cli-darwin-arm64 differ diff --git a/api/bin/dify-cli-linux-amd64 b/api/bin/dify-cli-linux-amd64 index 311454e20a..7d9de734c3 100755 Binary files a/api/bin/dify-cli-linux-amd64 and b/api/bin/dify-cli-linux-amd64 differ diff --git a/api/bin/dify-cli-linux-arm64 b/api/bin/dify-cli-linux-arm64 index eb4334679b..c0d5abd6d9 100755 Binary files a/api/bin/dify-cli-linux-arm64 and b/api/bin/dify-cli-linux-arm64 differ diff --git a/api/core/app/entities/app_invoke_entities.py b/api/core/app/entities/app_invoke_entities.py index 5bc453420d..cbc2cf61e5 100644 --- a/api/core/app/entities/app_invoke_entities.py +++ b/api/core/app/entities/app_invoke_entities.py @@ -36,6 +36,9 @@ class InvokeFrom(StrEnum): # this is used for plugin trigger and webhook trigger. TRIGGER = "trigger" + # AGENT indicates that this invocation is from an agent. + AGENT = "agent" + # EXPLORE indicates that this invocation is from # the workflow (or chatflow) explore page. EXPLORE = "explore" diff --git a/api/core/sandbox/bash_tool.py b/api/core/sandbox/bash_tool.py index 87b7f2bd1e..32c92a22e5 100644 --- a/api/core/sandbox/bash_tool.py +++ b/api/core/sandbox/bash_tool.py @@ -1,7 +1,7 @@ -import shlex from collections.abc import Generator from typing import Any +from core.sandbox.debug import sandbox_debug from core.tools.__base.tool import Tool from core.tools.__base.tool_runtime import ToolRuntime from core.tools.entities.common_entities import I18nObject @@ -68,7 +68,9 @@ class SandboxBashTool(Tool): connection_handle = self._sandbox.establish_connection() try: - cmd_list = shlex.split(command) + cmd_list = ["bash", "-c", command] + + sandbox_debug("bash_tool", "cmd_list", cmd_list) future = self._sandbox.run_command(connection_handle, cmd_list) timeout = COMMAND_TIMEOUT_SECONDS if COMMAND_TIMEOUT_SECONDS > 0 else None result = future.result(timeout=timeout) diff --git a/api/core/sandbox/debug.py b/api/core/sandbox/debug.py new file mode 100644 index 0000000000..e541bc435e --- /dev/null +++ b/api/core/sandbox/debug.py @@ -0,0 +1,18 @@ +"""Sandbox debug utilities. TODO: Remove this module when sandbox debugging is complete.""" + +from typing import Any + +from core.callback_handler.agent_tool_callback_handler import print_text + +SANDBOX_DEBUG_ENABLED = True + + +def sandbox_debug(tag: str, message: str, data: Any = None) -> None: + if not SANDBOX_DEBUG_ENABLED: + return + + print_text(f"\n[{tag}]\n", color="blue") + if data is not None: + print_text(f"{message}: {data}\n", color="blue") + else: + print_text(f"{message}\n", color="blue") diff --git a/api/core/sandbox/dify_cli.py b/api/core/sandbox/dify_cli.py index c8d502f148..4e43d58664 100644 --- a/api/core/sandbox/dify_cli.py +++ b/api/core/sandbox/dify_cli.py @@ -5,9 +5,10 @@ from typing import TYPE_CHECKING, Any from pydantic import BaseModel, Field +from core.model_runtime.utils.encoders import jsonable_encoder from core.sandbox.constants import DIFY_CLI_PATH_PATTERN from core.session.cli_api import CliApiSession -from core.tools.entities.tool_entities import ToolProviderType +from core.tools.entities.tool_entities import ToolParameter, ToolProviderType from core.virtual_environment.__base.entities import Arch, OperatingSystem if TYPE_CHECKING: @@ -77,11 +78,26 @@ class DifyCliToolConfig(BaseModel): def create_from_tool(cls, tool: Tool) -> DifyCliToolConfig: return cls( provider_type=cls.transform_provider_type(tool.tool_provider_type()), - identity=tool.entity.identity.model_dump(), - description=tool.entity.description.model_dump() if tool.entity.description else {}, - parameters=[param.model_dump() for param in tool.entity.parameters], + identity=to_json(tool.entity.identity), + description=to_json(tool.entity.description), + parameters=[cls.transform_parameter(parameter) for parameter in tool.entity.parameters], ) + @classmethod + def transform_parameter(cls, parameter: ToolParameter) -> dict[str, Any]: + transformed_parameter = to_json(parameter) + transformed_parameter.pop("input_schema", None) + transformed_parameter.pop("form", None) + match parameter.type: + case ( + ToolParameter.ToolParameterType.SYSTEM_FILES + | ToolParameter.ToolParameterType.FILE + | ToolParameter.ToolParameterType.FILES + ): + return transformed_parameter + case _: + return transformed_parameter + class DifyCliConfig(BaseModel): env: DifyCliEnvConfig @@ -104,6 +120,10 @@ class DifyCliConfig(BaseModel): ) +def to_json(obj: Any) -> dict[str, Any]: + return jsonable_encoder(obj, exclude_unset=True, exclude_defaults=True, exclude_none=True) + + __all__ = [ "DifyCliBinary", "DifyCliConfig", diff --git a/api/core/sandbox/session.py b/api/core/sandbox/session.py index b3ca581e62..9ed2b0589a 100644 --- a/api/core/sandbox/session.py +++ b/api/core/sandbox/session.py @@ -7,6 +7,7 @@ from types import TracebackType from core.sandbox.bash_tool import SandboxBashTool from core.sandbox.constants import DIFY_CLI_CONFIG_PATH, DIFY_CLI_PATH +from core.sandbox.debug import sandbox_debug from core.sandbox.dify_cli import DifyCliConfig from core.sandbox.manager import SandboxManager from core.session.cli_api import CliApiSessionManager @@ -46,6 +47,7 @@ class SandboxSession: config = DifyCliConfig.create(session, self._tools) config_json = json.dumps(config.model_dump(mode="json"), ensure_ascii=False) + sandbox_debug("sandbox", "config_json", config_json) sandbox.upload_file(DIFY_CLI_CONFIG_PATH, BytesIO(config_json.encode("utf-8"))) connection_handle = sandbox.establish_connection() diff --git a/api/core/workflow/nodes/command/node.py b/api/core/workflow/nodes/command/node.py index e753f8dc0f..a5d5347d0b 100644 --- a/api/core/workflow/nodes/command/node.py +++ b/api/core/workflow/nodes/command/node.py @@ -4,6 +4,7 @@ import shlex from collections.abc import Mapping, Sequence from typing import Any +from core.sandbox.debug import sandbox_debug from core.sandbox.manager import SandboxManager from core.virtual_environment.__base.command_future import CommandCancelledError, CommandTimeoutError from core.virtual_environment.__base.virtual_environment import VirtualEnvironment @@ -83,6 +84,9 @@ class CommandNode(Node[CommandNodeData]): try: command = shlex.split(raw_command) + + sandbox_debug("command_node", "command", command) + future = sandbox.run_command(connection_handle, command, cwd=working_directory) result = future.result(timeout=timeout) diff --git a/api/core/workflow/nodes/llm/node.py b/api/core/workflow/nodes/llm/node.py index 7f513e2e16..d04fbd3e42 100644 --- a/api/core/workflow/nodes/llm/node.py +++ b/api/core/workflow/nodes/llm/node.py @@ -13,7 +13,7 @@ from sqlalchemy import select from core.agent.entities import AgentEntity, AgentLog, AgentResult, AgentToolEntity, ExecutionContext from core.agent.patterns import StrategyFactory -from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity +from core.app.entities.app_invoke_entities import InvokeFrom, ModelConfigWithCredentialsEntity from core.file import File, FileTransferMethod, FileType, file_manager from core.helper.code_executor import CodeExecutor, CodeLanguage from core.llm_generator.output_parser.errors import OutputParserError @@ -1581,6 +1581,35 @@ class LLMNode(Node[LLMNodeData]): result = yield from self._process_tool_outputs(outputs) return result + def _prepare_sandbox_tools(self) -> list[Tool]: + """Prepare sandbox tools.""" + tool_instances = [] + + for tool in self._node_data.tools or []: + try: + # Get tool runtime from ToolManager + tool_runtime = ToolManager.get_tool_runtime( + tenant_id=self.tenant_id, + tool_name=tool.tool_name, + provider_id=tool.provider_name, + provider_type=tool.type, + invoke_from=InvokeFrom.AGENT, + credential_id=tool.credential_id, + ) + + # Apply custom description from extra field if available + if tool.extra.get("description") and tool_runtime.entity.description: + tool_runtime.entity.description.llm = ( + tool.extra.get("description") or tool_runtime.entity.description.llm + ) + + tool_instances.append(tool_runtime) + except Exception as e: + logger.warning("Failed to load tool %s: %s", tool, str(e)) + continue + + return tool_instances + def _invoke_llm_with_sandbox( self, model_instance: ModelInstance, @@ -1592,7 +1621,7 @@ class LLMNode(Node[LLMNodeData]): if not workflow_execution_id: raise LLMNodeError("workflow_execution_id is required for sandbox runtime mode") - configured_tools = self._prepare_tool_instances(variable_pool) + configured_tools = self._prepare_sandbox_tools() result: LLMGenerationData | None = None