mirror of
https://github.com/langgenius/dify.git
synced 2026-07-30 00:39:34 +08:00
431 lines
15 KiB
Python
431 lines
15 KiB
Python
"""Resolve product locators to ACTIVE Workspace Bindings and proxy file access."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import urllib.parse
|
|
from collections.abc import Callable
|
|
from typing import Any, Literal, cast
|
|
|
|
from dify_agent.client import Client
|
|
from dify_agent.layers.execution_context import (
|
|
DifyExecutionContextAgentConfigVersionKind,
|
|
DifyExecutionContextLayerConfig,
|
|
)
|
|
from dify_agent.protocol import WorkspaceListResponse, WorkspaceReadResponse, WorkspaceUploadRequest
|
|
from pydantic import BaseModel
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from configs import dify_config
|
|
from core.app.file_access import DatabaseFileAccessController
|
|
from core.app.workflow.file_runtime import DifyWorkflowFileRuntime
|
|
from core.db.session_factory import session_factory
|
|
from factories import file_factory
|
|
from models.agent import (
|
|
Agent,
|
|
AgentConfigDraft,
|
|
AgentConfigDraftType,
|
|
AgentWorkspaceBinding,
|
|
AgentWorkspaceOwnerType,
|
|
)
|
|
from models.model import App, Conversation
|
|
from models.workflow import WorkflowNodeExecutionModel
|
|
from services.agent.roster_service import AgentRosterService
|
|
from services.agent.workspace_service import AgentWorkspaceService, WorkspaceOwnerScope
|
|
|
|
|
|
class AgentSandboxInspectorError(Exception):
|
|
code: str
|
|
message: str
|
|
status_code: int
|
|
|
|
def __init__(self, code: str, message: str, *, status_code: int = 400) -> None:
|
|
super().__init__(message)
|
|
self.code = code
|
|
self.message = message
|
|
self.status_code = status_code
|
|
|
|
|
|
class AgentSandboxInfo(BaseModel):
|
|
workspace_cwd: str
|
|
|
|
|
|
class AgentSandboxUploadDownload(BaseModel):
|
|
url: str
|
|
|
|
|
|
class AgentAppSandboxService:
|
|
def __init__(self, *, client_factory: Callable[[], Client] | None = None) -> None:
|
|
self._client_factory = client_factory or _default_client_factory
|
|
|
|
def get_info(
|
|
self,
|
|
*,
|
|
tenant_id: str,
|
|
app_id: str,
|
|
agent_id: str,
|
|
caller_type: Literal["conversation", "build_draft"],
|
|
caller_id: str,
|
|
account_id: str,
|
|
) -> AgentSandboxInfo:
|
|
self._resolve_binding(
|
|
tenant_id=tenant_id,
|
|
app_id=app_id,
|
|
agent_id=agent_id,
|
|
caller_type=caller_type,
|
|
caller_id=caller_id,
|
|
account_id=account_id,
|
|
)
|
|
return AgentSandboxInfo(workspace_cwd=".")
|
|
|
|
def list_files(
|
|
self,
|
|
*,
|
|
tenant_id: str,
|
|
app_id: str,
|
|
agent_id: str,
|
|
caller_type: Literal["conversation", "build_draft"],
|
|
caller_id: str,
|
|
account_id: str,
|
|
path: str,
|
|
) -> WorkspaceListResponse:
|
|
binding = self._resolve_binding(
|
|
tenant_id=tenant_id,
|
|
app_id=app_id,
|
|
agent_id=agent_id,
|
|
caller_type=caller_type,
|
|
caller_id=caller_id,
|
|
account_id=account_id,
|
|
)
|
|
with self._client_factory() as client:
|
|
return client.list_workspace_files_sync(binding.backend_binding_ref, path)
|
|
|
|
def read_file(
|
|
self,
|
|
*,
|
|
tenant_id: str,
|
|
app_id: str,
|
|
agent_id: str,
|
|
caller_type: Literal["conversation", "build_draft"],
|
|
caller_id: str,
|
|
account_id: str,
|
|
path: str,
|
|
) -> WorkspaceReadResponse:
|
|
binding = self._resolve_binding(
|
|
tenant_id=tenant_id,
|
|
app_id=app_id,
|
|
agent_id=agent_id,
|
|
caller_type=caller_type,
|
|
caller_id=caller_id,
|
|
account_id=account_id,
|
|
)
|
|
with self._client_factory() as client:
|
|
return client.read_workspace_file_sync(binding.backend_binding_ref, path)
|
|
|
|
def upload_file(
|
|
self,
|
|
*,
|
|
tenant_id: str,
|
|
app_id: str,
|
|
agent_id: str,
|
|
caller_type: Literal["conversation", "build_draft"],
|
|
caller_id: str,
|
|
account_id: str,
|
|
path: str,
|
|
) -> AgentSandboxUploadDownload:
|
|
binding = self._resolve_binding(
|
|
tenant_id=tenant_id,
|
|
app_id=app_id,
|
|
agent_id=agent_id,
|
|
caller_type=caller_type,
|
|
caller_id=caller_id,
|
|
account_id=account_id,
|
|
)
|
|
with self._client_factory() as client:
|
|
uploaded = client.upload_workspace_file_sync(
|
|
WorkspaceUploadRequest(
|
|
backend_binding_ref=binding.backend_binding_ref,
|
|
path=path,
|
|
execution_context=DifyExecutionContextLayerConfig(
|
|
tenant_id=tenant_id,
|
|
app_id=app_id,
|
|
conversation_id=caller_id if caller_type == "conversation" else None,
|
|
agent_id=agent_id,
|
|
agent_config_version_id=binding.agent_config_version_id,
|
|
agent_config_version_kind=cast(
|
|
DifyExecutionContextAgentConfigVersionKind,
|
|
binding.agent_config_version_kind.value,
|
|
),
|
|
agent_mode="agent_app",
|
|
invoke_from="debugger",
|
|
),
|
|
)
|
|
)
|
|
return _upload_download_response(tenant_id=tenant_id, file_mapping=uploaded.file.model_dump(mode="python"))
|
|
|
|
@staticmethod
|
|
def _resolve_binding(
|
|
*,
|
|
tenant_id: str,
|
|
app_id: str,
|
|
agent_id: str,
|
|
caller_type: Literal["conversation", "build_draft"],
|
|
caller_id: str,
|
|
account_id: str,
|
|
) -> AgentWorkspaceBinding:
|
|
with session_factory.create_session() as session:
|
|
caller: AgentConfigDraft | Conversation | None
|
|
if caller_type == "build_draft":
|
|
agent = session.scalar(
|
|
select(Agent).where(
|
|
Agent.id == agent_id,
|
|
Agent.tenant_id == tenant_id,
|
|
)
|
|
)
|
|
if agent is None or AgentRosterService.runtime_backing_app_id(agent) != app_id:
|
|
caller = None
|
|
else:
|
|
caller = session.scalar(
|
|
select(AgentConfigDraft).where(
|
|
AgentConfigDraft.id == caller_id,
|
|
AgentConfigDraft.tenant_id == tenant_id,
|
|
AgentConfigDraft.agent_id == agent_id,
|
|
AgentConfigDraft.account_id == account_id,
|
|
AgentConfigDraft.draft_type == AgentConfigDraftType.DEBUG_BUILD,
|
|
)
|
|
)
|
|
owner_scope = WorkspaceOwnerScope(
|
|
tenant_id=tenant_id,
|
|
app_id=app_id,
|
|
owner_type=AgentWorkspaceOwnerType.BUILD_DRAFT,
|
|
owner_id=caller_id,
|
|
)
|
|
else:
|
|
caller = session.scalar(
|
|
select(Conversation)
|
|
.join(App, App.id == Conversation.app_id)
|
|
.where(
|
|
App.tenant_id == tenant_id,
|
|
Conversation.app_id == app_id,
|
|
Conversation.id == caller_id,
|
|
Conversation.from_account_id == account_id,
|
|
Conversation.is_deleted.is_(False),
|
|
)
|
|
)
|
|
owner_scope = WorkspaceOwnerScope(
|
|
tenant_id=tenant_id,
|
|
app_id=app_id,
|
|
owner_type=AgentWorkspaceOwnerType.CONVERSATION,
|
|
owner_id=caller_id,
|
|
)
|
|
if caller is None or caller.agent_workspace_binding_id is None:
|
|
raise AgentSandboxInspectorError(
|
|
"no_active_binding",
|
|
"this caller has no active Agent Workspace Binding",
|
|
status_code=404,
|
|
)
|
|
binding = AgentWorkspaceService.get_active_binding(
|
|
session=session,
|
|
tenant_id=tenant_id,
|
|
binding_id=caller.agent_workspace_binding_id,
|
|
expected_owner_scope=owner_scope,
|
|
)
|
|
if binding is None or binding.agent_id != agent_id:
|
|
raise AgentSandboxInspectorError(
|
|
"no_active_binding",
|
|
"this caller has no active Agent Workspace Binding",
|
|
status_code=404,
|
|
)
|
|
session.expunge(binding)
|
|
return binding
|
|
|
|
|
|
class WorkflowAgentSandboxService:
|
|
def __init__(self, *, client_factory: Callable[[], Client] | None = None) -> None:
|
|
self._client_factory = client_factory or _default_client_factory
|
|
|
|
def list_files(
|
|
self,
|
|
*,
|
|
tenant_id: str,
|
|
app_id: str,
|
|
workflow_run_id: str,
|
|
node_id: str,
|
|
node_execution_id: str,
|
|
path: str,
|
|
session: Session,
|
|
) -> WorkspaceListResponse:
|
|
binding = self._resolve_binding(
|
|
tenant_id=tenant_id,
|
|
app_id=app_id,
|
|
workflow_run_id=workflow_run_id,
|
|
node_id=node_id,
|
|
node_execution_id=node_execution_id,
|
|
session=session,
|
|
)
|
|
with self._client_factory() as client:
|
|
return client.list_workspace_files_sync(binding.backend_binding_ref, path)
|
|
|
|
def read_file(
|
|
self,
|
|
*,
|
|
tenant_id: str,
|
|
app_id: str,
|
|
workflow_run_id: str,
|
|
node_id: str,
|
|
node_execution_id: str,
|
|
path: str,
|
|
session: Session,
|
|
) -> WorkspaceReadResponse:
|
|
binding = self._resolve_binding(
|
|
tenant_id=tenant_id,
|
|
app_id=app_id,
|
|
workflow_run_id=workflow_run_id,
|
|
node_id=node_id,
|
|
node_execution_id=node_execution_id,
|
|
session=session,
|
|
)
|
|
with self._client_factory() as client:
|
|
return client.read_workspace_file_sync(binding.backend_binding_ref, path)
|
|
|
|
def upload_file(
|
|
self,
|
|
*,
|
|
tenant_id: str,
|
|
app_id: str,
|
|
workflow_run_id: str,
|
|
node_id: str,
|
|
node_execution_id: str,
|
|
path: str,
|
|
session: Session,
|
|
) -> AgentSandboxUploadDownload:
|
|
binding = self._resolve_binding(
|
|
tenant_id=tenant_id,
|
|
app_id=app_id,
|
|
workflow_run_id=workflow_run_id,
|
|
node_id=node_id,
|
|
node_execution_id=node_execution_id,
|
|
session=session,
|
|
)
|
|
with self._client_factory() as client:
|
|
uploaded = client.upload_workspace_file_sync(
|
|
WorkspaceUploadRequest(
|
|
backend_binding_ref=binding.backend_binding_ref,
|
|
path=path,
|
|
execution_context=DifyExecutionContextLayerConfig(
|
|
tenant_id=tenant_id,
|
|
app_id=app_id,
|
|
workflow_run_id=workflow_run_id,
|
|
node_id=node_id,
|
|
agent_id=binding.agent_id,
|
|
agent_config_version_id=binding.agent_config_version_id,
|
|
agent_config_version_kind=cast(
|
|
DifyExecutionContextAgentConfigVersionKind,
|
|
binding.agent_config_version_kind.value,
|
|
),
|
|
agent_mode="workflow_run",
|
|
invoke_from="debugger",
|
|
),
|
|
)
|
|
)
|
|
return _upload_download_response(tenant_id=tenant_id, file_mapping=uploaded.file.model_dump(mode="python"))
|
|
|
|
@staticmethod
|
|
def _resolve_binding(
|
|
*,
|
|
tenant_id: str,
|
|
app_id: str,
|
|
workflow_run_id: str,
|
|
node_id: str,
|
|
node_execution_id: str,
|
|
session: Session,
|
|
) -> AgentWorkspaceBinding:
|
|
execution = session.scalar(
|
|
select(WorkflowNodeExecutionModel).where(
|
|
WorkflowNodeExecutionModel.id == node_execution_id,
|
|
WorkflowNodeExecutionModel.tenant_id == tenant_id,
|
|
WorkflowNodeExecutionModel.app_id == app_id,
|
|
WorkflowNodeExecutionModel.workflow_run_id == workflow_run_id,
|
|
WorkflowNodeExecutionModel.node_id == node_id,
|
|
)
|
|
)
|
|
process_data = execution.process_data_dict if execution is not None else None
|
|
workflow_agent_binding_id = process_data.get("workflow_agent_binding_id") if process_data is not None else None
|
|
if (
|
|
execution is None
|
|
or execution.agent_workspace_binding_id is None
|
|
or not isinstance(workflow_agent_binding_id, str)
|
|
):
|
|
raise AgentSandboxInspectorError(
|
|
"no_active_binding",
|
|
"this Workflow Agent node execution has no active Workspace Binding",
|
|
status_code=404,
|
|
)
|
|
binding = AgentWorkspaceService.get_active_binding(
|
|
session=session,
|
|
tenant_id=tenant_id,
|
|
binding_id=execution.agent_workspace_binding_id,
|
|
expected_owner_scope=WorkspaceOwnerScope(
|
|
tenant_id=tenant_id,
|
|
app_id=app_id,
|
|
owner_type=AgentWorkspaceOwnerType.WORKFLOW_RUN,
|
|
owner_id=workflow_run_id,
|
|
owner_scope_key=f"{node_id}:{workflow_agent_binding_id}",
|
|
),
|
|
)
|
|
if binding is None:
|
|
raise AgentSandboxInspectorError(
|
|
"no_active_binding",
|
|
"this Workflow Agent node execution has no active Workspace Binding",
|
|
status_code=404,
|
|
)
|
|
return binding
|
|
|
|
|
|
def _upload_download_response(*, tenant_id: str, file_mapping: dict[str, Any]) -> AgentSandboxUploadDownload:
|
|
controller = DatabaseFileAccessController()
|
|
runtime = DifyWorkflowFileRuntime(file_access_controller=controller)
|
|
try:
|
|
file = file_factory.build_from_mapping(mapping=file_mapping, tenant_id=tenant_id, access_controller=controller)
|
|
url = runtime.resolve_file_url(file=file, for_external=True)
|
|
except ValueError as exc:
|
|
raise AgentSandboxInspectorError(
|
|
"workspace_upload_download_unavailable",
|
|
"uploaded Workspace file could not be converted to a download URL",
|
|
status_code=502,
|
|
) from exc
|
|
if not url:
|
|
raise AgentSandboxInspectorError(
|
|
"workspace_upload_download_unavailable",
|
|
"uploaded Workspace file does not support download URL generation",
|
|
status_code=502,
|
|
)
|
|
return AgentSandboxUploadDownload(url=_with_as_attachment(url))
|
|
|
|
|
|
def _with_as_attachment(url: str) -> str:
|
|
parsed = urllib.parse.urlsplit(url)
|
|
query = urllib.parse.parse_qsl(parsed.query, keep_blank_values=True)
|
|
query.append(("as_attachment", "true"))
|
|
return urllib.parse.urlunsplit(parsed._replace(query=urllib.parse.urlencode(query)))
|
|
|
|
|
|
def _default_client_factory() -> Client:
|
|
base_url = dify_config.AGENT_BACKEND_BASE_URL
|
|
if not base_url:
|
|
raise AgentSandboxInspectorError(
|
|
"inspector_unavailable",
|
|
"the Workspace file inspector is not available (Agent backend not configured)",
|
|
status_code=503,
|
|
)
|
|
return Client(base_url=base_url)
|
|
|
|
|
|
__all__ = [
|
|
"AgentAppSandboxService",
|
|
"AgentSandboxInfo",
|
|
"AgentSandboxInspectorError",
|
|
"AgentSandboxUploadDownload",
|
|
"WorkflowAgentSandboxService",
|
|
]
|