mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 10:28:10 +08:00
feat(sandbox): skill initialize & draft run
This commit is contained in:
@ -8,10 +8,12 @@ from .bash.dify_cli import (
|
||||
from .constants import (
|
||||
APP_ASSETS_PATH,
|
||||
APP_ASSETS_ZIP_PATH,
|
||||
DIFY_CLI_CONFIG_PATH,
|
||||
DIFY_CLI_CONFIG_FILENAME,
|
||||
DIFY_CLI_GLOBAL_TOOLS_PATH,
|
||||
DIFY_CLI_PATH,
|
||||
DIFY_CLI_PATH_PATTERN,
|
||||
DIFY_CLI_ROOT,
|
||||
DIFY_CLI_TOOLS_ROOT,
|
||||
)
|
||||
from .initializer import AppAssetsInitializer, DifyCliInitializer, SandboxInitializer
|
||||
from .manager import SandboxManager
|
||||
@ -24,10 +26,12 @@ from .vm import SandboxBuilder, SandboxType, VMConfig
|
||||
__all__ = [
|
||||
"APP_ASSETS_PATH",
|
||||
"APP_ASSETS_ZIP_PATH",
|
||||
"DIFY_CLI_CONFIG_PATH",
|
||||
"DIFY_CLI_CONFIG_FILENAME",
|
||||
"DIFY_CLI_GLOBAL_TOOLS_PATH",
|
||||
"DIFY_CLI_PATH",
|
||||
"DIFY_CLI_PATH_PATTERN",
|
||||
"DIFY_CLI_ROOT",
|
||||
"DIFY_CLI_TOOLS_ROOT",
|
||||
"AppAssetsInitializer",
|
||||
"ArchiveSandboxStorage",
|
||||
"DifyCliBinary",
|
||||
|
||||
@ -21,8 +21,9 @@ COMMAND_TIMEOUT_SECONDS = 60
|
||||
|
||||
|
||||
class SandboxBashTool(Tool):
|
||||
def __init__(self, sandbox: VirtualEnvironment, tenant_id: str):
|
||||
def __init__(self, sandbox: VirtualEnvironment, tenant_id: str, tools_path: str) -> None:
|
||||
self._sandbox = sandbox
|
||||
self._tools_path = tools_path
|
||||
|
||||
entity = ToolEntity(
|
||||
identity=ToolIdentity(
|
||||
@ -71,9 +72,10 @@ class SandboxBashTool(Tool):
|
||||
try:
|
||||
with with_connection(self._sandbox) as conn:
|
||||
cmd_list = ["bash", "-c", command]
|
||||
env_vars = {"PATH": f"{self._tools_path}:/usr/local/bin:/usr/bin:/bin"}
|
||||
|
||||
sandbox_debug("bash_tool", "cmd_list", cmd_list)
|
||||
future = submit_command(self._sandbox, conn, cmd_list)
|
||||
future = submit_command(self._sandbox, conn, cmd_list, environments=env_vars)
|
||||
timeout = COMMAND_TIMEOUT_SECONDS if COMMAND_TIMEOUT_SECONDS > 0 else None
|
||||
result = future.result(timeout=timeout)
|
||||
|
||||
|
||||
@ -6,8 +6,11 @@ DIFY_CLI_PATH: Final[str] = "/tmp/.dify/bin/dify"
|
||||
|
||||
DIFY_CLI_PATH_PATTERN: Final[str] = "dify-cli-{os}-{arch}"
|
||||
|
||||
DIFY_CLI_CONFIG_PATH: Final[str] = "/tmp/.dify/.dify_cli.json"
|
||||
DIFY_CLI_CONFIG_FILENAME: Final[str] = ".dify_cli.json"
|
||||
|
||||
DIFY_CLI_TOOLS_ROOT: Final[str] = "/tmp/.dify/tools"
|
||||
DIFY_CLI_GLOBAL_TOOLS_PATH: Final[str] = "/tmp/.dify/tools/global"
|
||||
|
||||
# App Assets (relative path - stays in sandbox workdir)
|
||||
APP_ASSETS_PATH: Final[str] = "assets"
|
||||
APP_ASSETS_ZIP_PATH: Final[str] = "/tmp/.dify/tmp/assets.zip"
|
||||
APP_ASSETS_ZIP_PATH: Final[str] = "/tmp/assets.zip"
|
||||
|
||||
@ -1,33 +1,25 @@
|
||||
import logging
|
||||
from io import BytesIO
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.app_assets.paths import AssetPaths
|
||||
from core.virtual_environment.__base.helpers import execute, with_connection
|
||||
from core.virtual_environment.__base.virtual_environment import VirtualEnvironment
|
||||
from extensions.ext_database import db
|
||||
from extensions.ext_storage import storage
|
||||
from models.app_asset import AppAssets
|
||||
|
||||
from ..constants import APP_ASSETS_PATH, APP_ASSETS_ZIP_PATH, DIFY_CLI_ROOT
|
||||
from ..constants import APP_ASSETS_PATH, APP_ASSETS_ZIP_PATH
|
||||
from .base import SandboxInitializer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AppAssetsInitializer(SandboxInitializer):
|
||||
def __init__(self, tenant_id: str, app_id: str) -> None:
|
||||
def __init__(self, tenant_id: str, app_id: str, assets_id: str) -> None:
|
||||
self._tenant_id = tenant_id
|
||||
self._app_id = app_id
|
||||
self._assets_id = assets_id
|
||||
|
||||
def initialize(self, env: VirtualEnvironment) -> None:
|
||||
published = self._get_latest_published()
|
||||
if not published:
|
||||
logger.debug("No published assets for app_id=%s, skipping", self._app_id)
|
||||
return
|
||||
|
||||
zip_key = AssetPaths.published_zip(self._tenant_id, self._app_id, published.id)
|
||||
zip_key = AssetPaths.build_zip(self._tenant_id, self._app_id, self._assets_id)
|
||||
try:
|
||||
zip_data = storage.load_once(zip_key)
|
||||
except Exception:
|
||||
@ -42,18 +34,6 @@ class AppAssetsInitializer(SandboxInitializer):
|
||||
env.upload_file(APP_ASSETS_ZIP_PATH, BytesIO(zip_data))
|
||||
|
||||
with with_connection(env) as conn:
|
||||
execute(
|
||||
env,
|
||||
["mkdir", "-p", f"{DIFY_CLI_ROOT}/tmp"],
|
||||
connection=conn,
|
||||
error_message="Failed to create temp directory",
|
||||
)
|
||||
execute(
|
||||
env,
|
||||
["mkdir", "-p", APP_ASSETS_PATH],
|
||||
connection=conn,
|
||||
error_message="Failed to create assets directory",
|
||||
)
|
||||
execute(
|
||||
env,
|
||||
["unzip", "-o", APP_ASSETS_ZIP_PATH, "-d", APP_ASSETS_PATH],
|
||||
@ -71,18 +51,5 @@ class AppAssetsInitializer(SandboxInitializer):
|
||||
logger.info(
|
||||
"App assets initialized for app_id=%s, published_id=%s",
|
||||
self._app_id,
|
||||
published.id,
|
||||
self._assets_id,
|
||||
)
|
||||
|
||||
def _get_latest_published(self) -> AppAssets | None:
|
||||
with Session(db.engine) as session:
|
||||
return (
|
||||
session.query(AppAssets)
|
||||
.filter(
|
||||
AppAssets.tenant_id == self._tenant_id,
|
||||
AppAssets.app_id == self._app_id,
|
||||
AppAssets.version != AppAssets.VERSION_DRAFT,
|
||||
)
|
||||
.order_by(AppAssets.created_at.desc())
|
||||
.first()
|
||||
)
|
||||
|
||||
@ -1,37 +1,147 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
|
||||
from core.virtual_environment.__base.helpers import execute
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from core.app_assets.entities import ToolType
|
||||
from core.session.cli_api import CliApiSessionManager
|
||||
from core.skill.entities import ToolManifest
|
||||
from core.skill.skill_manager import SkillManager
|
||||
from core.tools.__base.tool import Tool
|
||||
from core.tools.entities.tool_entities import ToolProviderType
|
||||
from core.tools.tool_manager import ToolManager
|
||||
from core.virtual_environment.__base.helpers import execute, with_connection
|
||||
from core.virtual_environment.__base.virtual_environment import VirtualEnvironment
|
||||
|
||||
from ..bash.dify_cli import DifyCliLocator
|
||||
from ..constants import DIFY_CLI_PATH, DIFY_CLI_ROOT
|
||||
from ..bash.dify_cli import DifyCliConfig, DifyCliLocator
|
||||
from ..constants import (
|
||||
DIFY_CLI_CONFIG_FILENAME,
|
||||
DIFY_CLI_GLOBAL_TOOLS_PATH,
|
||||
DIFY_CLI_PATH,
|
||||
DIFY_CLI_ROOT,
|
||||
)
|
||||
from .base import SandboxInitializer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DifyCliInitializer(SandboxInitializer):
|
||||
def __init__(self, cli_root: str | Path | None = None) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
tenant_id: str,
|
||||
app_id: str,
|
||||
assets_id: str,
|
||||
cli_root: str | Path | None = None,
|
||||
) -> None:
|
||||
self._tenant_id = tenant_id
|
||||
self._app_id = app_id
|
||||
self._assets_id = assets_id
|
||||
self._locator = DifyCliLocator(root=cli_root)
|
||||
|
||||
self._tools = []
|
||||
self._cli_api_session = None
|
||||
|
||||
def initialize(self, env: VirtualEnvironment) -> None:
|
||||
binary = self._locator.resolve(env.metadata.os, env.metadata.arch)
|
||||
|
||||
execute(
|
||||
env,
|
||||
["mkdir", "-p", f"{DIFY_CLI_ROOT}/bin"],
|
||||
timeout=10,
|
||||
error_message="Failed to create dify CLI directory",
|
||||
)
|
||||
with with_connection(env) as conn:
|
||||
execute(
|
||||
env,
|
||||
["mkdir", "-p", f"{DIFY_CLI_ROOT}/bin"],
|
||||
connection=conn,
|
||||
timeout=10,
|
||||
error_message="Failed to create dify CLI directory",
|
||||
)
|
||||
|
||||
env.upload_file(DIFY_CLI_PATH, BytesIO(binary.path.read_bytes()))
|
||||
env.upload_file(DIFY_CLI_PATH, BytesIO(binary.path.read_bytes()))
|
||||
|
||||
execute(
|
||||
env,
|
||||
["chmod", "+x", DIFY_CLI_PATH],
|
||||
timeout=10,
|
||||
error_message="Failed to mark dify CLI as executable",
|
||||
)
|
||||
logger.info("Dify CLI uploaded to sandbox, path=%s", DIFY_CLI_PATH)
|
||||
execute(
|
||||
env,
|
||||
["chmod", "+x", DIFY_CLI_PATH],
|
||||
connection=conn,
|
||||
timeout=10,
|
||||
error_message="Failed to mark dify CLI as executable",
|
||||
)
|
||||
|
||||
logger.info("Dify CLI uploaded to sandbox, path=%s", DIFY_CLI_PATH)
|
||||
|
||||
manifest = SkillManager.load_tool_manifest(
|
||||
self._tenant_id,
|
||||
self._app_id,
|
||||
self._assets_id,
|
||||
)
|
||||
|
||||
if manifest is None or not manifest.tools:
|
||||
logger.info("No tools found in manifest for assets_id=%s", self._assets_id)
|
||||
return
|
||||
|
||||
self._tools = self._resolve_tools_from_manifest(manifest)
|
||||
self._cli_api_session = CliApiSessionManager().create(tenant_id=self._tenant_id, user_id="system")
|
||||
|
||||
execute(
|
||||
env,
|
||||
["mkdir", "-p", DIFY_CLI_GLOBAL_TOOLS_PATH],
|
||||
connection=conn,
|
||||
timeout=10,
|
||||
error_message="Failed to create global tools directory",
|
||||
)
|
||||
|
||||
config_json = json.dumps(
|
||||
DifyCliConfig.create(self._cli_api_session, self._tools).model_dump(mode="json"), ensure_ascii=False
|
||||
)
|
||||
env.upload_file(
|
||||
f"{DIFY_CLI_GLOBAL_TOOLS_PATH}/{DIFY_CLI_CONFIG_FILENAME}", BytesIO(config_json.encode("utf-8"))
|
||||
)
|
||||
|
||||
execute(
|
||||
env,
|
||||
[DIFY_CLI_PATH, "init"],
|
||||
connection=conn,
|
||||
timeout=30,
|
||||
cwd=DIFY_CLI_GLOBAL_TOOLS_PATH,
|
||||
error_message="Failed to initialize Dify CLI",
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Global tools initialized, path=%s, tool_count=%d",
|
||||
DIFY_CLI_GLOBAL_TOOLS_PATH,
|
||||
len(self._tools),
|
||||
)
|
||||
|
||||
def _resolve_tools_from_manifest(self, manifest: ToolManifest) -> list[Tool]:
|
||||
tools: list[Tool] = []
|
||||
|
||||
for entry in manifest.tools.values():
|
||||
if entry.provider is None or entry.tool_name is None:
|
||||
logger.warning("Skipping tool entry with missing provider or tool_name: %s", entry.uuid)
|
||||
continue
|
||||
|
||||
try:
|
||||
provider_type = self._convert_tool_type(entry.type)
|
||||
tool = ToolManager.get_tool_runtime(
|
||||
tenant_id=self._tenant_id,
|
||||
provider_type=provider_type,
|
||||
provider_id=entry.provider,
|
||||
tool_name=entry.tool_name,
|
||||
invoke_from=InvokeFrom.AGENT,
|
||||
credential_id=entry.credential_id,
|
||||
)
|
||||
tools.append(tool)
|
||||
except Exception as e:
|
||||
logger.warning("Failed to resolve tool %s/%s: %s", entry.provider, entry.tool_name, e)
|
||||
continue
|
||||
|
||||
return tools
|
||||
|
||||
@staticmethod
|
||||
def _convert_tool_type(tool_type: ToolType) -> ToolProviderType:
|
||||
match tool_type:
|
||||
case ToolType.BUILTIN:
|
||||
return ToolProviderType.BUILT_IN
|
||||
case ToolType.MCP:
|
||||
return ToolProviderType.MCP
|
||||
case _:
|
||||
raise ValueError(f"Unsupported tool type: {tool_type}")
|
||||
|
||||
@ -1,79 +1,90 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from io import BytesIO
|
||||
from types import TracebackType
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from core.session.cli_api import CliApiSessionManager
|
||||
from core.virtual_environment.__base.helpers import execute
|
||||
from core.virtual_environment.__base.virtual_environment import VirtualEnvironment
|
||||
|
||||
from .bash.dify_cli import DifyCliConfig
|
||||
from .constants import DIFY_CLI_CONFIG_PATH, DIFY_CLI_PATH
|
||||
from .constants import (
|
||||
DIFY_CLI_GLOBAL_TOOLS_PATH,
|
||||
)
|
||||
from .manager import SandboxManager
|
||||
from .utils.debug import sandbox_debug
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.tools.__base.tool import Tool
|
||||
|
||||
from .bash.bash_tool import SandboxBashTool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SandboxSession:
|
||||
_workflow_execution_id: str
|
||||
_tenant_id: str
|
||||
_user_id: str
|
||||
_node_id: str | None
|
||||
_allow_tools: list[str] | None
|
||||
|
||||
_sandbox: VirtualEnvironment | None
|
||||
_bash_tool: SandboxBashTool | None
|
||||
_session_id: str | None
|
||||
_tools_path: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
workflow_execution_id: str,
|
||||
tenant_id: str,
|
||||
user_id: str,
|
||||
tools: list[Tool],
|
||||
node_id: str | None = None,
|
||||
allow_tools: list[str] | None = None,
|
||||
) -> None:
|
||||
self._workflow_execution_id = workflow_execution_id
|
||||
self._tenant_id = tenant_id
|
||||
self._user_id = user_id
|
||||
self._tools = tools
|
||||
self._node_id = node_id
|
||||
self._allow_tools = allow_tools
|
||||
|
||||
self._sandbox: VirtualEnvironment | None = None
|
||||
self._bash_tool: SandboxBashTool | None = None
|
||||
self._session_id: str | None = None
|
||||
self._sandbox = None
|
||||
self._bash_tool = None
|
||||
self._session_id = None
|
||||
self._tools_path = DIFY_CLI_GLOBAL_TOOLS_PATH
|
||||
|
||||
def __enter__(self) -> SandboxSession:
|
||||
sandbox = SandboxManager.get(self._workflow_execution_id)
|
||||
if sandbox is None:
|
||||
raise RuntimeError(f"Sandbox not found for workflow_execution_id={self._workflow_execution_id}")
|
||||
|
||||
session = CliApiSessionManager().create(tenant_id=self._tenant_id, user_id=self._user_id)
|
||||
self._session_id = session.id
|
||||
self._sandbox = sandbox
|
||||
|
||||
try:
|
||||
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")))
|
||||
|
||||
execute(
|
||||
sandbox,
|
||||
[DIFY_CLI_PATH, "init"],
|
||||
timeout=30,
|
||||
error_message="Failed to initialize Dify CLI in sandbox",
|
||||
)
|
||||
|
||||
except Exception:
|
||||
CliApiSessionManager().delete(session.id)
|
||||
self._session_id = None
|
||||
raise
|
||||
if self._allow_tools is not None:
|
||||
# TODO: Implement node tools directory setup
|
||||
if self._node_id is None:
|
||||
raise ValueError("node_id is required when allow_tools is specified")
|
||||
# self._tools_path = self._setup_node_tools_directory(sandbox, self._node_id, self._allow_tools)
|
||||
else:
|
||||
self._tools_path = DIFY_CLI_GLOBAL_TOOLS_PATH
|
||||
|
||||
from .bash.bash_tool import SandboxBashTool
|
||||
|
||||
self._sandbox = sandbox
|
||||
self._bash_tool = SandboxBashTool(sandbox=sandbox, tenant_id=self._tenant_id)
|
||||
self._bash_tool = SandboxBashTool(sandbox=sandbox, tenant_id=self._tenant_id, tools_path=self._tools_path)
|
||||
return self
|
||||
|
||||
def _setup_node_tools_directory(
|
||||
self,
|
||||
sandbox: VirtualEnvironment,
|
||||
node_id: str,
|
||||
allow_tools: list[str],
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _get_tool_name_from_config(tool_config: dict) -> str:
|
||||
identity = tool_config.get("identity", {})
|
||||
provider = identity.get("provider", "")
|
||||
name = identity.get("name", "")
|
||||
return f"{provider}__{name}"
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
|
||||
Reference in New Issue
Block a user