mirror of
https://github.com/langgenius/dify.git
synced 2026-01-14 06:07:33 +08:00
feat: sandbox session and dify cli
This commit is contained in:
parent
ce0a59b60d
commit
3d2840edb6
@ -712,3 +712,7 @@ ANNOTATION_IMPORT_MAX_CONCURRENT=5
|
||||
SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD=21
|
||||
SANDBOX_EXPIRED_RECORDS_CLEAN_BATCH_SIZE=1000
|
||||
SANDBOX_EXPIRED_RECORDS_RETENTION_DAYS=30
|
||||
|
||||
# Sandbox Dify CLI configuration
|
||||
# Directory containing dify CLI binaries (dify-cli-<os>-<arch>). Defaults to api/bin when unset.
|
||||
SANDBOX_DIFY_CLI_ROOT=
|
||||
|
||||
BIN
api/bin/dify-cli-darwin-amd64
Executable file
BIN
api/bin/dify-cli-darwin-amd64
Executable file
Binary file not shown.
BIN
api/bin/dify-cli-darwin-arm64
Executable file
BIN
api/bin/dify-cli-darwin-arm64
Executable file
Binary file not shown.
BIN
api/bin/dify-cli-linux-amd64
Executable file
BIN
api/bin/dify-cli-linux-amd64
Executable file
Binary file not shown.
BIN
api/bin/dify-cli-linux-arm64
Executable file
BIN
api/bin/dify-cli-linux-arm64
Executable file
Binary file not shown.
@ -2,6 +2,7 @@ import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic.fields import FieldInfo
|
||||
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict, TomlConfigSettingsSource
|
||||
|
||||
@ -82,6 +83,14 @@ class DifyConfig(
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
SANDBOX_DIFY_CLI_ROOT: str | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Filesystem directory containing dify CLI binaries named dify-cli-<os>-<arch>. "
|
||||
"Defaults to api/bin when unset."
|
||||
),
|
||||
)
|
||||
|
||||
# Before adding any config,
|
||||
# please consider to arrange it in the proper config group of existed or added
|
||||
# for better readability and maintainability.
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import logging
|
||||
from collections.abc import Mapping
|
||||
from io import BytesIO
|
||||
from typing import Any
|
||||
|
||||
from core.sandbox import DIFY_CLI_PATH, DifyCliLocator
|
||||
from core.virtual_environment.__base.virtual_environment import VirtualEnvironment
|
||||
from core.virtual_environment.sandbox_manager import SandboxManager
|
||||
from core.workflow.graph_engine.layers.base import GraphEngineLayer
|
||||
@ -62,10 +64,29 @@ class SandboxLayer(GraphEngineLayer):
|
||||
sandbox.metadata.id,
|
||||
sandbox.metadata.arch,
|
||||
)
|
||||
self._upload_cli(sandbox)
|
||||
except Exception as e:
|
||||
logger.exception("Failed to initialize sandbox")
|
||||
raise SandboxInitializationError(f"Failed to initialize sandbox: {e}") from e
|
||||
|
||||
def _upload_cli(self, sandbox: VirtualEnvironment) -> None:
|
||||
locator = DifyCliLocator()
|
||||
binary = locator.resolve(sandbox.metadata.os, sandbox.metadata.arch)
|
||||
|
||||
sandbox.upload_file(DIFY_CLI_PATH, BytesIO(binary.path.read_bytes()))
|
||||
|
||||
connection_handle = sandbox.establish_connection()
|
||||
try:
|
||||
future = sandbox.run_command(connection_handle, ["chmod", "+x", DIFY_CLI_PATH])
|
||||
result = future.result(timeout=10)
|
||||
if result.exit_code not in (0, None):
|
||||
stderr = result.stderr.decode("utf-8", errors="replace") if result.stderr else ""
|
||||
raise RuntimeError(f"Failed to mark dify CLI as executable: {stderr}")
|
||||
|
||||
logger.info("Dify CLI uploaded to sandbox, path=%s", DIFY_CLI_PATH)
|
||||
finally:
|
||||
sandbox.release_connection(connection_handle)
|
||||
|
||||
def on_event(self, event: GraphEngineEvent) -> None:
|
||||
pass
|
||||
|
||||
|
||||
27
api/core/sandbox/__init__.py
Normal file
27
api/core/sandbox/__init__.py
Normal file
@ -0,0 +1,27 @@
|
||||
from core.sandbox.constants import (
|
||||
DIFY_CLI_CONFIG_PATH,
|
||||
DIFY_CLI_PATH,
|
||||
DIFY_CLI_PATH_PATTERN,
|
||||
SANDBOX_WORK_DIR,
|
||||
)
|
||||
from core.sandbox.dify_cli import (
|
||||
DifyCliBinary,
|
||||
DifyCliConfig,
|
||||
DifyCliEnvConfig,
|
||||
DifyCliLocator,
|
||||
DifyCliToolConfig,
|
||||
)
|
||||
from core.sandbox.session import SandboxSession
|
||||
|
||||
__all__ = [
|
||||
"DIFY_CLI_CONFIG_PATH",
|
||||
"DIFY_CLI_PATH",
|
||||
"DIFY_CLI_PATH_PATTERN",
|
||||
"SANDBOX_WORK_DIR",
|
||||
"DifyCliBinary",
|
||||
"DifyCliConfig",
|
||||
"DifyCliEnvConfig",
|
||||
"DifyCliLocator",
|
||||
"DifyCliToolConfig",
|
||||
"SandboxSession",
|
||||
]
|
||||
9
api/core/sandbox/constants.py
Normal file
9
api/core/sandbox/constants.py
Normal file
@ -0,0 +1,9 @@
|
||||
from typing import Final
|
||||
|
||||
SANDBOX_WORK_DIR: Final[str] = "/work"
|
||||
|
||||
DIFY_CLI_PATH: Final[str] = "/work/.dify/bin/dify"
|
||||
|
||||
DIFY_CLI_PATH_PATTERN: Final[str] = "dify-cli-{os}-{arch}"
|
||||
|
||||
DIFY_CLI_CONFIG_PATH: Final[str] = "/work/config.json"
|
||||
95
api/core/sandbox/dify_cli.py
Normal file
95
api/core/sandbox/dify_cli.py
Normal file
@ -0,0 +1,95 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from core.sandbox.constants import DIFY_CLI_PATH_PATTERN
|
||||
from core.virtual_environment.__base.entities import Arch, OperatingSystem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.tools.__base.tool import Tool
|
||||
|
||||
|
||||
class DifyCliBinary(BaseModel):
|
||||
operating_system: OperatingSystem = Field(alias="os")
|
||||
arch: Arch
|
||||
path: Path
|
||||
|
||||
model_config = {
|
||||
"populate_by_name": True,
|
||||
"arbitrary_types_allowed": True,
|
||||
}
|
||||
|
||||
|
||||
class DifyCliLocator:
|
||||
def __init__(self, root: str | Path | None = None) -> None:
|
||||
from configs import dify_config
|
||||
|
||||
if root is not None:
|
||||
self._root = Path(root)
|
||||
elif dify_config.SANDBOX_DIFY_CLI_ROOT:
|
||||
self._root = Path(dify_config.SANDBOX_DIFY_CLI_ROOT)
|
||||
else:
|
||||
api_root = Path(__file__).resolve().parents[2]
|
||||
self._root = api_root / "bin"
|
||||
|
||||
def resolve(self, operating_system: OperatingSystem, arch: Arch) -> DifyCliBinary:
|
||||
filename = DIFY_CLI_PATH_PATTERN.format(os=operating_system.value, arch=arch.value)
|
||||
candidate = self._root / filename
|
||||
if not candidate.is_file():
|
||||
raise FileNotFoundError(
|
||||
f"dify CLI binary not found: {candidate}. Configure SANDBOX_DIFY_CLI_ROOT or ensure the file exists."
|
||||
)
|
||||
|
||||
return DifyCliBinary(os=operating_system, arch=arch, path=candidate)
|
||||
|
||||
|
||||
class DifyCliEnvConfig(BaseModel):
|
||||
files_url: str
|
||||
inner_api_url: str
|
||||
inner_api_session_id: str
|
||||
|
||||
|
||||
class DifyCliToolConfig(BaseModel):
|
||||
provider_type: str
|
||||
identity: dict[str, Any]
|
||||
description: dict[str, Any]
|
||||
parameters: list[dict[str, Any]]
|
||||
|
||||
@classmethod
|
||||
def create_from_tool(cls, tool: Tool) -> DifyCliToolConfig:
|
||||
return cls(
|
||||
provider_type=tool.tool_provider_type().value,
|
||||
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],
|
||||
)
|
||||
|
||||
|
||||
class DifyCliConfig(BaseModel):
|
||||
env: DifyCliEnvConfig
|
||||
tools: list[DifyCliToolConfig]
|
||||
|
||||
@classmethod
|
||||
def create(cls, session_id: str, tools: list[Tool]) -> DifyCliConfig:
|
||||
from configs import dify_config
|
||||
|
||||
return cls(
|
||||
env=DifyCliEnvConfig(
|
||||
files_url=dify_config.FILES_URL,
|
||||
inner_api_url=dify_config.CONSOLE_API_URL,
|
||||
inner_api_session_id=session_id,
|
||||
),
|
||||
tools=[DifyCliToolConfig.create_from_tool(tool) for tool in tools],
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"DifyCliBinary",
|
||||
"DifyCliConfig",
|
||||
"DifyCliEnvConfig",
|
||||
"DifyCliLocator",
|
||||
"DifyCliToolConfig",
|
||||
]
|
||||
@ -3,7 +3,10 @@ from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
from io import BytesIO
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from core.sandbox.constants import DIFY_CLI_CONFIG_PATH, DIFY_CLI_PATH
|
||||
from core.sandbox.dify_cli import DifyCliConfig
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from types import TracebackType
|
||||
@ -43,15 +46,30 @@ class SandboxSession:
|
||||
raise RuntimeError(f"Sandbox not found for workflow_execution_id={self._workflow_execution_id}")
|
||||
|
||||
session = InnerApiSessionManager().create(tenant_id=self._tenant_id, user_id=self._user_id)
|
||||
self._session_id = session.id
|
||||
|
||||
try:
|
||||
_upload_and_init_dify_cli(sandbox, self._tools, session.id)
|
||||
config = DifyCliConfig.create(self._session_id, self._tools)
|
||||
config_json = json.dumps(config.model_dump(mode="json"), ensure_ascii=False)
|
||||
|
||||
sandbox.upload_file(DIFY_CLI_CONFIG_PATH, BytesIO(config_json.encode("utf-8")))
|
||||
|
||||
connection_handle = sandbox.establish_connection()
|
||||
try:
|
||||
future = sandbox.run_command(connection_handle, [DIFY_CLI_PATH, "init"])
|
||||
result = future.result(timeout=30)
|
||||
if result.exit_code not in (0, None):
|
||||
stderr = result.stderr.decode("utf-8", errors="replace") if result.stderr else ""
|
||||
raise RuntimeError(f"Failed to initialize Dify CLI in sandbox: {stderr}")
|
||||
finally:
|
||||
sandbox.release_connection(connection_handle)
|
||||
|
||||
except Exception:
|
||||
InnerApiSessionManager().delete(session.id)
|
||||
self._session_id = None
|
||||
raise
|
||||
|
||||
self._sandbox = sandbox
|
||||
self._session_id = session.id
|
||||
self._bash_tool = SandboxBashTool(sandbox=sandbox, tenant_id=self._tenant_id)
|
||||
return self
|
||||
|
||||
@ -82,44 +100,3 @@ class SandboxSession:
|
||||
InnerApiSessionManager().delete(self._session_id)
|
||||
logger.debug("Cleaned up SandboxSession session_id=%s", self._session_id)
|
||||
self._session_id = None
|
||||
|
||||
|
||||
def _upload_and_init_dify_cli(sandbox: VirtualEnvironment, tools: list[Tool], session_id: str) -> None:
|
||||
from configs import dify_config
|
||||
|
||||
config = {
|
||||
"env": {
|
||||
"files_url": dify_config.FILES_URL,
|
||||
"inner_api_url": dify_config.CONSOLE_API_URL,
|
||||
"inner_api_session_id": session_id,
|
||||
},
|
||||
"tools": _serialize_tools(tools),
|
||||
}
|
||||
|
||||
config_json = json.dumps(config, ensure_ascii=False)
|
||||
config_path = f"/tmp/dify-init-{session_id}.json"
|
||||
|
||||
sandbox.upload_file(config_path, BytesIO(config_json.encode("utf-8")))
|
||||
|
||||
connection_handle = sandbox.establish_connection()
|
||||
try:
|
||||
future = sandbox.run_command(connection_handle, ["dify", "init", config_path])
|
||||
result = future.result(timeout=30)
|
||||
if result.exit_code != 0:
|
||||
stderr = result.stderr.decode("utf-8", errors="replace") if result.stderr else ""
|
||||
raise RuntimeError(f"Failed to initialize Dify CLI in sandbox: {stderr}")
|
||||
finally:
|
||||
sandbox.release_connection(connection_handle)
|
||||
|
||||
|
||||
def _serialize_tools(tools: list[Tool]) -> list[dict[str, Any]]:
|
||||
result: list[dict[str, Any]] = []
|
||||
|
||||
for tool in tools:
|
||||
tool_config = tool.entity.model_dump()
|
||||
tool_config["provider_type"] = tool.tool_provider_type().value
|
||||
tool_config["credential_type"] = tool.runtime.credential_type.value if tool.runtime else "default"
|
||||
tool_config["credential_id"] = tool.runtime.tool_id if tool.runtime else tool.entity.identity.provider
|
||||
result.append(tool_config)
|
||||
|
||||
return result
|
||||
@ -13,7 +13,6 @@ from sqlalchemy import select
|
||||
|
||||
from core.agent.entities import AgentLog, AgentResult, AgentToolEntity, ExecutionContext
|
||||
from core.agent.patterns import StrategyFactory
|
||||
from core.agent.sandbox_session import SandboxSession
|
||||
from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity
|
||||
from core.file import File, FileTransferMethod, FileType, file_manager
|
||||
from core.helper.code_executor import CodeExecutor, CodeLanguage
|
||||
@ -51,6 +50,7 @@ from core.model_runtime.utils.encoders import jsonable_encoder
|
||||
from core.prompt.entities.advanced_prompt_entities import CompletionModelPromptTemplate, MemoryConfig
|
||||
from core.prompt.utils.prompt_message_util import PromptMessageUtil
|
||||
from core.rag.entities.citation_metadata import RetrievalSourceMetadata
|
||||
from core.sandbox import SandboxSession
|
||||
from core.tools.__base.tool import Tool
|
||||
from core.tools.signature import sign_upload_file
|
||||
from core.tools.tool_manager import ToolManager
|
||||
|
||||
Loading…
Reference in New Issue
Block a user