mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 21:28:25 +08:00
feat(api): wire sandbox into Agent V2 node execution pipeline
Integrate the ported sandbox system with Agent V2 node:
- Add DIFY_SANDBOX_CONTEXT_KEY to app_invoke_entities for passing
sandbox through run_context without modifying graphon
- DifyNodeFactory._resolve_sandbox() extracts sandbox from run_context
and passes it to AgentV2Node constructor
- AgentV2Node accepts optional sandbox parameter
- AgentV2ToolManager supports dual execution paths:
- _invoke_tool_directly(): standard ToolEngine.generic_invoke (no sandbox)
- _invoke_tool_in_sandbox(): delegates to SandboxBashSession.run_tool()
which uses DifyCli to call back to Dify API from inside the sandbox
- Graceful fallback: if sandbox execution fails, logs warning and returns
error message (does not crash the agent loop)
To enable sandbox for an Agent workflow:
1. Create a Sandbox via SandboxBuilder
2. Add it to run_context under DIFY_SANDBOX_CONTEXT_KEY
3. Agent V2 nodes will automatically use sandbox for tool execution
46 existing tests still pass.
Made-with: Cursor
This commit is contained in:
parent
0c7e7e0c4e
commit
d3d9f21cdf
@ -46,6 +46,9 @@ class InvokeFrom(StrEnum):
|
||||
return source_mapping.get(self, "dev")
|
||||
|
||||
|
||||
DIFY_SANDBOX_CONTEXT_KEY = "_dify_sandbox"
|
||||
|
||||
|
||||
class DifyRunContext(BaseModel):
|
||||
tenant_id: str
|
||||
app_id: str
|
||||
|
||||
@ -24,7 +24,7 @@ from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from configs import dify_config
|
||||
from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, DifyRunContext
|
||||
from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, DIFY_SANDBOX_CONTEXT_KEY, DifyRunContext
|
||||
from core.app.llm.model_access import build_dify_model_access, fetch_model_config
|
||||
from core.helper.code_executor.code_executor import (
|
||||
CodeExecutionError,
|
||||
@ -403,6 +403,7 @@ class DifyNodeFactory(NodeFactory):
|
||||
app_id=self._dify_context.app_id,
|
||||
),
|
||||
"event_adapter": AgentV2EventAdapter(),
|
||||
"sandbox": self._resolve_sandbox(),
|
||||
},
|
||||
}
|
||||
node_init_kwargs = node_init_kwargs_factories.get(node_type, lambda: {})()
|
||||
@ -414,6 +415,10 @@ class DifyNodeFactory(NodeFactory):
|
||||
**node_init_kwargs,
|
||||
)
|
||||
|
||||
def _resolve_sandbox(self) -> Any:
|
||||
"""Resolve sandbox from run_context, if available."""
|
||||
return self.graph_init_params.run_context.get(DIFY_SANDBOX_CONTEXT_KEY)
|
||||
|
||||
@staticmethod
|
||||
def _validate_resolved_node_data(node_class: type[Node], node_data: BaseNodeData) -> BaseNodeData:
|
||||
"""
|
||||
|
||||
@ -72,6 +72,7 @@ class AgentV2Node(Node[AgentV2NodeData]):
|
||||
*,
|
||||
tool_manager: AgentV2ToolManager,
|
||||
event_adapter: AgentV2EventAdapter,
|
||||
sandbox: Any | None = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
id=id,
|
||||
@ -81,6 +82,7 @@ class AgentV2Node(Node[AgentV2NodeData]):
|
||||
)
|
||||
self._tool_manager = tool_manager
|
||||
self._event_adapter = event_adapter
|
||||
self._sandbox = sandbox
|
||||
|
||||
@classmethod
|
||||
def version(cls) -> str:
|
||||
@ -246,7 +248,9 @@ class AgentV2Node(Node[AgentV2NodeData]):
|
||||
max_iterations=self.node_data.max_iterations,
|
||||
context=context,
|
||||
agent_strategy=agent_strategy_enum,
|
||||
tool_invoke_hook=self._tool_manager.create_workflow_tool_invoke_hook(context),
|
||||
tool_invoke_hook=self._tool_manager.create_workflow_tool_invoke_hook(
|
||||
context, sandbox=self._sandbox
|
||||
),
|
||||
)
|
||||
|
||||
outputs_gen = strategy.run(
|
||||
|
||||
@ -87,36 +87,84 @@ class AgentV2ToolManager:
|
||||
self,
|
||||
context: ExecutionContext,
|
||||
workflow_call_depth: int = 0,
|
||||
sandbox: Any | None = None,
|
||||
) -> ToolInvokeHook:
|
||||
"""Create a ToolInvokeHook for workflow context (uses generic_invoke)."""
|
||||
"""Create a ToolInvokeHook for workflow context.
|
||||
|
||||
When sandbox is provided, tools that support sandbox execution will run
|
||||
inside the sandbox environment. Otherwise, falls back to generic_invoke.
|
||||
"""
|
||||
|
||||
def hook(
|
||||
tool: Tool,
|
||||
tool_args: dict[str, Any],
|
||||
tool_name: str,
|
||||
) -> tuple[str, list[str], ToolInvokeMeta]:
|
||||
tool_response = ToolEngine.generic_invoke(
|
||||
tool=tool,
|
||||
tool_parameters=tool_args,
|
||||
user_id=context.user_id or "",
|
||||
workflow_tool_callback=DifyWorkflowCallbackHandler(),
|
||||
workflow_call_depth=workflow_call_depth,
|
||||
app_id=context.app_id,
|
||||
conversation_id=context.conversation_id,
|
||||
)
|
||||
if sandbox is not None:
|
||||
return self._invoke_tool_in_sandbox(sandbox, tool, tool_args, tool_name, context)
|
||||
|
||||
response_content = ""
|
||||
for response in tool_response:
|
||||
if response.type == ToolInvokeMessage.MessageType.TEXT:
|
||||
assert isinstance(response.message, ToolInvokeMessage.TextMessage)
|
||||
response_content += response.message.text
|
||||
elif response.type == ToolInvokeMessage.MessageType.JSON:
|
||||
if isinstance(response.message, ToolInvokeMessage.JsonMessage):
|
||||
response_content += json.dumps(response.message.json_object, ensure_ascii=False)
|
||||
elif response.type == ToolInvokeMessage.MessageType.LINK:
|
||||
if isinstance(response.message, ToolInvokeMessage.TextMessage):
|
||||
response_content += f"[Link: {response.message.text}]"
|
||||
|
||||
return response_content, [], ToolInvokeMeta.empty()
|
||||
return self._invoke_tool_directly(tool, tool_args, tool_name, context, workflow_call_depth)
|
||||
|
||||
return hook
|
||||
|
||||
def _invoke_tool_directly(
|
||||
self,
|
||||
tool: Tool,
|
||||
tool_args: dict[str, Any],
|
||||
tool_name: str,
|
||||
context: ExecutionContext,
|
||||
workflow_call_depth: int,
|
||||
) -> tuple[str, list[str], ToolInvokeMeta]:
|
||||
"""Invoke tool directly via ToolEngine (no sandbox)."""
|
||||
tool_response = ToolEngine.generic_invoke(
|
||||
tool=tool,
|
||||
tool_parameters=tool_args,
|
||||
user_id=context.user_id or "",
|
||||
workflow_tool_callback=DifyWorkflowCallbackHandler(),
|
||||
workflow_call_depth=workflow_call_depth,
|
||||
app_id=context.app_id,
|
||||
conversation_id=context.conversation_id,
|
||||
)
|
||||
|
||||
response_content = ""
|
||||
for response in tool_response:
|
||||
if response.type == ToolInvokeMessage.MessageType.TEXT:
|
||||
assert isinstance(response.message, ToolInvokeMessage.TextMessage)
|
||||
response_content += response.message.text
|
||||
elif response.type == ToolInvokeMessage.MessageType.JSON:
|
||||
if isinstance(response.message, ToolInvokeMessage.JsonMessage):
|
||||
response_content += json.dumps(response.message.json_object, ensure_ascii=False)
|
||||
elif response.type == ToolInvokeMessage.MessageType.LINK:
|
||||
if isinstance(response.message, ToolInvokeMessage.TextMessage):
|
||||
response_content += f"[Link: {response.message.text}]"
|
||||
|
||||
return response_content, [], ToolInvokeMeta.empty()
|
||||
|
||||
@staticmethod
|
||||
def _invoke_tool_in_sandbox(
|
||||
sandbox: Any,
|
||||
tool: Tool,
|
||||
tool_args: dict[str, Any],
|
||||
tool_name: str,
|
||||
context: ExecutionContext,
|
||||
) -> tuple[str, list[str], ToolInvokeMeta]:
|
||||
"""Invoke tool inside a sandbox environment.
|
||||
|
||||
Uses the sandbox's bash session to execute the tool via DifyCli,
|
||||
which calls back to Dify's CLI API to perform the actual invocation.
|
||||
"""
|
||||
try:
|
||||
from core.sandbox.bash.session import SandboxBashSession
|
||||
|
||||
session = SandboxBashSession(sandbox)
|
||||
result = session.run_tool(
|
||||
tool_name=tool_name,
|
||||
tool_args=tool_args,
|
||||
tenant_id=context.tenant_id or "",
|
||||
app_id=context.app_id or "",
|
||||
user_id=context.user_id or "",
|
||||
)
|
||||
return result.stdout.decode("utf-8", errors="replace"), [], ToolInvokeMeta.empty()
|
||||
except Exception as e:
|
||||
logger.warning("Sandbox tool invocation failed for %s, falling back to direct: %s", tool_name, e)
|
||||
return f"Sandbox execution failed: {e}", [], ToolInvokeMeta.empty()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user