Files
dify/api/core/tools/tool_manager.py
T

1560 lines
68 KiB
Python

import json
import logging
import mimetypes
import time
from collections.abc import Callable, Generator, Mapping
from os import listdir, path
from threading import Lock
from typing import TYPE_CHECKING, Any, ClassVar, Literal, Protocol, cast
import sqlalchemy as sa
from pydantic import TypeAdapter
from sqlalchemy import select
from sqlalchemy.orm import Session, sessionmaker
from typing_extensions import TypedDict
from yarl import URL
import contexts
from configs import dify_config
from core.agent.entities import AgentToolEntity
from core.app.entities.app_invoke_entities import InvokeFrom
from core.dify_builder.models import Actor
from core.dify_builder.real_call_consent import (
EXPECTED_PLUGIN_IDENTIFIER_HEADER,
ConfirmedToolGuard,
validate_confirmed_dispatch_headers,
)
from core.entities import PluginCredentialType
from core.helper.module_import_helper import load_single_subclass_from_source
from core.helper.position_helper import is_filtered
from core.helper.provider_cache import NoOpProviderCredentialCache, ToolProviderCredentialsCache
from core.plugin.entities.plugin_daemon import CredentialType
from core.plugin.impl.exc import PluginDaemonNotFoundError, PluginNotFoundError
from core.plugin.impl.tool import PluginToolManager
from core.tools.__base.tool import Tool
from core.tools.__base.tool_provider import ToolProviderController
from core.tools.__base.tool_runtime import ToolRuntime
from core.tools.builtin_tool.provider import BuiltinToolProviderController
from core.tools.builtin_tool.providers._positions import BuiltinToolProviderSort
from core.tools.builtin_tool.tool import BuiltinTool
from core.tools.custom_tool.provider import ApiToolProviderController
from core.tools.custom_tool.tool import ApiTool
from core.tools.entities.api_entities import ToolProviderApiEntity, ToolProviderTypeApiLiteral
from core.tools.entities.common_entities import I18nObject
from core.tools.entities.tool_entities import (
ApiProviderAuthType,
ApiProviderSchemaType,
EmojiIconDict,
ToolInvokeFrom,
ToolParameter,
ToolProviderType,
emoji_icon_adapter,
)
from core.tools.errors import ToolProviderCredentialValidationError, ToolProviderNotFoundError
from core.tools.mcp_tool.provider import MCPToolProviderController
from core.tools.mcp_tool.tool import MCPTool
from core.tools.plugin_tool.provider import PluginToolProviderController
from core.tools.plugin_tool.tool import PluginTool
from core.tools.tool_label_manager import ToolLabelManager
from core.tools.utils.configuration import ToolParameterConfigurationManager
from core.tools.utils.encryption import create_provider_encrypter, create_tool_provider_encrypter
from core.tools.utils.uuid_utils import is_valid_uuid
from core.tools.workflow_as_tool.provider import WorkflowToolProviderController
from core.tools.workflow_as_tool.tool import WorkflowTool
from extensions.ext_database import db
from graphon.runtime import VariablePool
from graphon.variables.template_resolution import convert_template
from models.provider_ids import ToolProviderID
from models.tools import ApiToolProvider, BuiltinToolProvider, WorkflowToolProvider
from runtime_contracts.builder.execution import (
BuilderExecutionPolicyError,
)
from services.tools.mcp_tools_manage_service import MCPToolManageService
from services.tools.tools_transform_service import ToolTransformService
if TYPE_CHECKING:
from runtime_contracts.workflow_driver import WorkflowRunDriver
logger = logging.getLogger(__name__)
class ApiProviderControllerItem(TypedDict):
provider: ApiToolProvider
controller: ApiToolProviderController
_credentials_adapter: TypeAdapter[dict[str, Any]] = TypeAdapter(dict[str, Any])
class WorkflowToolRuntimeSpec(Protocol):
@property
def provider_type(self) -> ToolProviderType: ...
@property
def provider_id(self) -> str: ...
@property
def tool_name(self) -> str: ...
@property
def tool_configurations(self) -> Mapping[str, Any]: ...
@property
def credential_id(self) -> str | None: ...
class WorkflowToolRuntimeKwargs(TypedDict, total=False):
"""Internal draft scope and authorization mode supplied by the native factory."""
draft_tool_workflow_id: str
allow_draft_credential_delegation: bool
builder_expected_credential_fingerprint: str
builder_expected_api_binding: str
builder_credential_refreshed: Callable[[str], None] | None
class _DraftToolRuntimeKwargs(WorkflowToolRuntimeKwargs, total=False):
draft_tool_app_id: str
draft_tool_node_id: str
class ToolCredentialResolver(Protocol):
def __call__(
self,
*,
controller: BuiltinToolProviderController,
provider_id: str,
workspace_id: str,
credential_id: str | None,
) -> tuple[Mapping[str, object], CredentialType]: ...
class ToolManager:
_builtin_provider_lock = Lock()
_hardcoded_providers: dict[str, BuiltinToolProviderController] = {}
_external_hardcoded_provider_classes: list[type[BuiltinToolProviderController]] = []
_builtin_providers_loaded = False
_builtin_tools_labels: dict[str, I18nObject | None] = {}
_credential_resolver: ClassVar[ToolCredentialResolver | None] = None
@classmethod
def configure_credential_resolver(cls, resolver: ToolCredentialResolver) -> None:
"""Register stateless composition; it must not capture an app, database or provider."""
cls._credential_resolver = resolver
@classmethod
def get_hardcoded_provider(cls, provider: str) -> BuiltinToolProviderController:
"""
get the hardcoded provider
"""
if len(cls._hardcoded_providers) == 0:
# init the builtin providers
cls.load_hardcoded_providers_cache()
return cls._hardcoded_providers[provider]
@classmethod
def get_builtin_provider(
cls, provider: str, tenant_id: str
) -> BuiltinToolProviderController | PluginToolProviderController:
"""
get the builtin provider
:param provider: the name of the provider
:param tenant_id: the id of the tenant
:return: the provider
"""
# split provider to
if len(cls._hardcoded_providers) == 0:
# init the builtin providers
cls.load_hardcoded_providers_cache()
if provider not in cls._hardcoded_providers:
# get plugin provider
plugin_provider = cls.get_plugin_provider(provider, tenant_id)
if plugin_provider:
return plugin_provider
return cls._hardcoded_providers[provider]
@classmethod
def get_plugin_provider(cls, provider: str, tenant_id: str) -> PluginToolProviderController:
"""
get the plugin provider
"""
# check if context is set
try:
contexts.plugin_tool_providers.get()
except LookupError:
contexts.plugin_tool_providers.set({})
contexts.plugin_tool_providers_lock.set(Lock())
plugin_tool_providers = contexts.plugin_tool_providers.get()
if provider in plugin_tool_providers:
return plugin_tool_providers[provider]
with contexts.plugin_tool_providers_lock.get():
# double check
plugin_tool_providers = contexts.plugin_tool_providers.get()
if provider in plugin_tool_providers:
return plugin_tool_providers[provider]
manager = PluginToolManager()
try:
provider_entity = manager.fetch_tool_provider(tenant_id, provider)
except (PluginNotFoundError, PluginDaemonNotFoundError) as exc:
# The plugin daemon rejected the provider name (probably an
# unknown or non-builtin identifier). Translate to the
# console's domain error so the API returns a 4xx instead
# of leaking the daemon's ``PluginNotFoundError`` as a 500.
raise ToolProviderNotFoundError(f"plugin provider {provider} not found") from exc
if not provider_entity:
raise ToolProviderNotFoundError(f"plugin provider {provider} not found")
controller = PluginToolProviderController(
entity=provider_entity.declaration,
plugin_id=provider_entity.plugin_id,
plugin_unique_identifier=provider_entity.plugin_unique_identifier,
tenant_id=tenant_id,
)
plugin_tool_providers[provider] = controller
return controller
@classmethod
def get_tool_runtime(
cls,
provider_type: ToolProviderType,
provider_id: str,
tool_name: str,
tenant_id: str,
user_id: str | None = None,
invoke_from: InvokeFrom = InvokeFrom.DEBUGGER,
tool_invoke_from: ToolInvokeFrom = ToolInvokeFrom.AGENT,
credential_id: str | None = None,
*,
confirmed_guard: ConfirmedToolGuard | None = None,
draft_tool_workflow_id: str | None = None,
draft_tool_node_id: str | None = None,
allow_draft_credential_delegation: bool = False,
builder_expected_credential_fingerprint: str | None = None,
builder_expected_api_binding: str | None = None,
builder_credential_refreshed: Callable[[str], None] | None = None,
draft_tool_app_id: str | None = None,
json_provider_validator: Callable[[object, object], None] | None = None,
execution_driver: "WorkflowRunDriver | None" = None,
) -> BuiltinTool | PluginTool | ApiTool | WorkflowTool | MCPTool:
"""
get the tool runtime
:param provider_type: the type of the provider
:param provider_id: the id of the provider
:param tool_name: the name of the tool
:param tenant_id: the tenant id
:param user_id: the caller id bound to runtime-scoped model/tool lookups
:param invoke_from: invoke from
:param tool_invoke_from: the tool invoke from
:param credential_id: the credential id
:param confirmed_guard: ``None`` for every ordinary native call (the
default keeps them unchanged); a consented Builder call takes the
separate confirmed branch before any credential, config or schema work
:param draft_tool_workflow_id: internal native-factory workflow scope;
ordinary debugger drafts recheck setup at Tool resolution. Published
children keep delegation even if debugger invocation is inherited.
:param draft_tool_app_id: actual native app owner for that workflow scope
:param draft_tool_node_id: actual node whose persisted credential binding is checked
:param allow_draft_credential_delegation: only ordinary native workflows may
reuse an explicit credential bound to this saved node; direct and Builder
callers retain actor-visible selection by default
:param json_provider_validator: trusted JSON service callback checking the
actual native controller and tool before any credential branch
:return: the tool
"""
if json_provider_validator is not None and (
confirmed_guard is not None
or draft_tool_workflow_id is not None
or draft_tool_app_id is not None
or draft_tool_node_id is not None
or allow_draft_credential_delegation
or credential_id is not None
or provider_type != ToolProviderType.BUILT_IN
):
raise BuilderExecutionPolicyError("invalid_json_tool_configuration")
if confirmed_guard is not None:
return cls._get_confirmed_tool_runtime(
confirmed_guard,
provider_type=provider_type,
provider_id=provider_id,
tool_name=tool_name,
tenant_id=tenant_id,
user_id=user_id,
invoke_from=invoke_from,
tool_invoke_from=tool_invoke_from,
credential_id=credential_id,
)
draft_selection = None
if provider_type == ToolProviderType.BUILT_IN and draft_tool_workflow_id is not None:
from services.tools.draft_tool_setup import prepare_draft_tool_runtime_selection
draft_selection = prepare_draft_tool_runtime_selection(
tenant_id=tenant_id,
app_id=draft_tool_app_id or "",
workflow_id=draft_tool_workflow_id,
session_maker=sessionmaker(bind=db.engine),
actor_id=user_id,
invoke_from=invoke_from,
provider=provider_id,
action=tool_name,
credential_id=credential_id,
node_id=draft_tool_node_id,
allow_workflow_delegation=allow_draft_credential_delegation,
expected_credential_fingerprint=builder_expected_credential_fingerprint,
on_credential_refreshed=builder_credential_refreshed,
)
match provider_type:
case ToolProviderType.BUILT_IN:
provider_controller = cls.get_builtin_provider(provider_id, tenant_id)
builtin_tool = provider_controller.get_tool(tool_name)
if not builtin_tool:
raise ToolProviderNotFoundError(f"builtin tool {tool_name} not found")
if json_provider_validator is not None:
# The service checks the actual (possibly cached) controller and
# returned tool before native credential selection or validation.
json_provider_validator(provider_controller, builtin_tool)
if json_provider_validator is not None or not provider_controller.need_credentials:
return builtin_tool.fork_tool_runtime(
runtime=ToolRuntime(
tenant_id=tenant_id,
user_id=user_id,
credentials={},
invoke_from=invoke_from,
tool_invoke_from=tool_invoke_from,
)
)
if draft_selection is not None:
from services.tools.ordinary_tool_credentials import resolve_ordinary_tool_credentials
runtime = resolve_ordinary_tool_credentials(
provider_controller=provider_controller,
provider_id=provider_id,
tenant_id=tenant_id,
user_id=user_id,
invoke_from=invoke_from,
tool_invoke_from=tool_invoke_from,
credential_id=credential_id,
draft_selection=draft_selection,
)
else:
runtime = cls._get_ordinary_tool_runtime(
provider_controller=provider_controller,
provider_id=provider_id,
tenant_id=tenant_id,
user_id=user_id,
invoke_from=invoke_from,
tool_invoke_from=tool_invoke_from,
credential_id=credential_id,
)
return builtin_tool.fork_tool_runtime(runtime=runtime)
case ToolProviderType.API:
if builder_expected_api_binding is None:
api_provider, credentials = cls.get_api_provider_controller(tenant_id, provider_id)
else:
api_provider, credentials = cls.get_api_provider_controller(
tenant_id, provider_id, expected_binding=builder_expected_api_binding, action=tool_name
)
if builder_expected_api_binding is None:
encrypter, _ = create_tool_provider_encrypter(tenant_id=tenant_id, controller=api_provider)
else:
# The approved ciphertext version cannot inherit a name-keyed cached account.
encrypter, _ = create_provider_encrypter(
tenant_id=tenant_id,
config=[item.to_basic_provider_config() for item in api_provider.get_credentials_schema()],
cache=NoOpProviderCredentialCache(),
)
return api_provider.get_tool(tool_name).fork_tool_runtime(
runtime=ToolRuntime(
tenant_id=tenant_id,
user_id=user_id,
credentials=dict(encrypter.decrypt(credentials)),
invoke_from=invoke_from,
tool_invoke_from=tool_invoke_from,
)
)
case ToolProviderType.WORKFLOW:
workflow_provider_stmt = select(WorkflowToolProvider).where(
WorkflowToolProvider.tenant_id == tenant_id, WorkflowToolProvider.id == provider_id
)
with Session(db.engine, expire_on_commit=False) as session, session.begin():
workflow_provider = session.scalar(workflow_provider_stmt)
if workflow_provider is None:
raise ToolProviderNotFoundError(f"workflow provider {provider_id} not found")
controller = ToolTransformService.workflow_provider_to_controller(db_provider=workflow_provider)
controller_tools: list[WorkflowTool] = controller.get_tools(tenant_id=workflow_provider.tenant_id)
if controller_tools is None or len(controller_tools) == 0:
raise ToolProviderNotFoundError(f"workflow provider {provider_id} not found")
workflow_tool = controller.get_tools(tenant_id=workflow_provider.tenant_id)[0].fork_tool_runtime(
runtime=ToolRuntime(
tenant_id=tenant_id,
user_id=user_id,
credentials={},
invoke_from=invoke_from,
tool_invoke_from=tool_invoke_from,
)
)
workflow_tool.execution_driver = execution_driver
return workflow_tool
case ToolProviderType.APP:
raise NotImplementedError("app provider not implemented")
case ToolProviderType.PLUGIN:
plugin_controller = cls.get_plugin_provider(provider_id, tenant_id)
plugin_tool = plugin_controller.get_tool(tool_name)
return plugin_tool.fork_tool_runtime(
runtime=cls._get_ordinary_tool_runtime(
provider_controller=plugin_controller,
provider_id=provider_id,
tenant_id=tenant_id,
user_id=user_id,
invoke_from=invoke_from,
tool_invoke_from=tool_invoke_from,
credential_id=credential_id,
)
)
case ToolProviderType.MCP:
mcp_tool = cls.get_mcp_provider_controller(tenant_id, provider_id).get_tool(tool_name)
runtime = getattr(mcp_tool, "runtime", None)
if runtime is not None:
runtime.user_id = user_id
runtime.invoke_from = invoke_from
runtime.tool_invoke_from = tool_invoke_from
return mcp_tool
case ToolProviderType.DATASET_RETRIEVAL:
raise ToolProviderNotFoundError(f"provider type {provider_type.value} not found")
case _:
raise ToolProviderNotFoundError(f"provider type {provider_type} not found")
@classmethod
def _get_ordinary_tool_runtime(
cls,
*,
provider_controller: BuiltinToolProviderController,
provider_id: str,
tenant_id: str,
user_id: str | None,
invoke_from: InvokeFrom,
tool_invoke_from: ToolInvokeFrom,
credential_id: str | None,
) -> ToolRuntime:
"""Resolve admitted ordinary calls with the configured credential service.
No-auth tools skip credential reads. Explicit plugin references must be
valid stored IDs; they cannot fall back to another workspace credential.
Draft and confirmed calls retain their separate admission and refresh owners.
"""
if not provider_controller.need_credentials:
return ToolRuntime(
tenant_id=tenant_id,
user_id=user_id,
credentials={},
invoke_from=invoke_from,
tool_invoke_from=tool_invoke_from,
)
selected_id = None
if isinstance(provider_controller, PluginToolProviderController) and credential_id:
if not is_valid_uuid(credential_id):
raise ToolProviderCredentialValidationError("Select a valid stored Tool credential reference.")
selected_id = credential_id
if cls._credential_resolver is None:
raise RuntimeError("Tool credential resolver is not configured")
credentials, credential_type = cls._credential_resolver(
controller=provider_controller,
provider_id=provider_id,
workspace_id=tenant_id,
credential_id=selected_id,
)
return ToolRuntime(
tenant_id=tenant_id,
user_id=user_id,
credentials=dict(credentials),
credential_type=credential_type,
runtime_parameters={},
invoke_from=invoke_from,
tool_invoke_from=tool_invoke_from,
)
@classmethod
def _get_confirmed_tool_runtime(
cls,
guard: ConfirmedToolGuard,
*,
provider_type: ToolProviderType,
provider_id: str,
tool_name: str,
tenant_id: str,
user_id: str | None,
invoke_from: InvokeFrom,
tool_invoke_from: ToolInvokeFrom,
credential_id: str | None,
) -> PluginTool:
"""One consented builtin plugin Tool bound to its exact explicit credential.
Unlike the ordinary branch this never reads the workspace default,
validates credentials, decrypts tool configuration or uses the ordinary
credential cache. The private credential/client snapshot is read in one
short session closed before compliance, decryption, refresh or dispatch.
A due token is refreshed at most once, only after the guard spends the
refresh phase, and the refreshed ciphertext is committed by the guard's
CAS before the ordinary cache is invalidated (outside SQL, as native
refresh does). A refresh that is not recorded leaves its auth effect
unknown and blocks the business phase. Refusals raise
``BuilderExecutionPolicyError`` without native text; nothing is retried.
"""
from core.helper.credential_utils import runtime_check_credential_policy_compliance
from core.plugin.impl.oauth import OAuthHandler
from core.plugin.plugin_service import PluginService
from services.tools.builtin_tools_manage_service import BuiltinToolManageService
context = guard.binding.context
if tenant_id != context.tenant_id:
raise BuilderExecutionPolicyError("execution_owner_mismatch")
if provider_type != ToolProviderType.BUILT_IN:
raise BuilderExecutionPolicyError("unsupported_action")
if credential_id is None or not is_valid_uuid(credential_id):
raise BuilderExecutionPolicyError("credential_required")
headers = validate_confirmed_dispatch_headers(guard.dispatch_headers())
artifact = headers[EXPECTED_PLUGIN_IDENTIFIER_HEADER]
try:
controller = cls.get_builtin_provider(provider_id, tenant_id)
tool = controller.get_tool(tool_name) if isinstance(controller, PluginToolProviderController) else None
except Exception:
# Daemon/transport text may echo provider data.
raise BuilderExecutionPolicyError("installed_tool_unavailable") from None
if (
not isinstance(controller, PluginToolProviderController)
or tool is None
or not controller.need_credentials
or controller.plugin_unique_identifier != artifact
or tool.plugin_unique_identifier != artifact
):
raise BuilderExecutionPolicyError("installed_tool_unavailable")
# Static declaration only: a dynamic schema would send credentials to the daemon before consent.
if tool.entity.has_runtime_parameters:
raise BuilderExecutionPolicyError("unsupported_tool_configuration")
try:
# Server-resolved evidence selects the same OAuth client the consent bound.
verified = PluginService.is_plugin_verified(tenant_id, artifact) is True
except Exception:
raise BuilderExecutionPolicyError("installed_tool_unavailable") from None
selection = BuiltinToolManageService.load_builder_credential(
actor=Actor(account_id=context.actor_id, tenant_id=tenant_id),
provider_id=provider_id,
credential_id=credential_id,
verified_plugin=verified,
)
snapshot = selection.snapshot
# Admit exactly the selected detached snapshot against the claimed grant before any
# compliance, decryption, refresh or dispatch; later phases use this same snapshot.
BuiltinToolManageService.validate_builder_credential_snapshot(
binding=guard.binding, reference=snapshot.reference
)
try:
runtime_check_credential_policy_compliance(
credential_id=credential_id,
provider=provider_id,
credential_type=PluginCredentialType.TOOL,
check_existence=False,
)
except BuilderExecutionPolicyError:
raise
except Exception:
raise BuilderExecutionPolicyError("credential_policy_refused") from None
try:
credential_type = CredentialType.of(snapshot.reference.credential_type)
stored = json.loads(snapshot.encrypted_credentials) if snapshot.encrypted_credentials else {}
except ValueError:
raise BuilderExecutionPolicyError("credential_reference_unavailable") from None
if not isinstance(stored, dict):
raise BuilderExecutionPolicyError("credential_reference_unavailable")
encrypter, _ = create_provider_encrypter(
tenant_id=tenant_id,
config=[x.to_basic_provider_config() for x in controller.get_credentials_schema_by_type(credential_type)],
cache=NoOpProviderCredentialCache(),
)
credentials: Mapping[str, Any] = encrypter.decrypt(stored)
expires_at = snapshot.reference.expires_at
if expires_at != -1 and (expires_at - 60) < int(time.time()):
tool_provider = ToolProviderID(provider_id)
system_credentials = BuiltinToolManageService.decrypt_builder_oauth_client(
tenant_id=tenant_id, provider_controller=controller, client=snapshot.client
)
guard.before_refresh()
recorded = False
try:
refreshed = OAuthHandler().refresh_credentials(
tenant_id=tenant_id,
user_id=selection.owner_user_id,
plugin_id=tool_provider.plugin_id,
provider=tool_provider.provider_name,
redirect_uri=f"{dify_config.CONSOLE_API_URL}/console/api/oauth/plugin/{provider_id}/tool/callback",
system_credentials=system_credentials,
credentials=credentials,
confirmed_dispatch_headers=headers,
)
guard.persist_refresh(json.dumps(encrypter.encrypt(refreshed.credentials)), refreshed.expires_at)
recorded = True
except Exception as error:
reason = error.reason_code if isinstance(error, BuilderExecutionPolicyError) else "refresh_failed"
raise BuilderExecutionPolicyError(reason) from None
finally:
if not recorded:
cls._finish_without_business(guard)
# The CAS committed; invalidate the ordinary cache outside SQL exactly as native refresh does.
try:
ToolProviderCredentialsCache(
tenant_id=tenant_id, provider=provider_id, credential_id=credential_id
).delete()
except Exception:
# The refreshed row stays committed; the grant closes as unknown before any
# business dispatch, without retrying, echoing or logging the cache error.
cls._finish_without_business(guard)
raise BuilderExecutionPolicyError("credential_cache_unavailable") from None
credentials = refreshed.credentials
return PluginTool(
entity=tool.entity,
runtime=ToolRuntime(
tenant_id=tenant_id,
user_id=user_id,
credentials=dict(credentials),
credential_type=credential_type,
runtime_parameters={},
invoke_from=invoke_from,
tool_invoke_from=tool_invoke_from,
),
tenant_id=tool.tenant_id,
icon=tool.icon,
plugin_unique_identifier=tool.plugin_unique_identifier,
confirmed_guard=guard,
)
@staticmethod
def _finish_without_business(guard: ConfirmedToolGuard) -> None:
"""Close a grant whose refresh phase ended without any business dispatch as unknown."""
try:
guard.finish_stream(complete=False, declared_failure=False)
except Exception as error:
# The grant stays unresolved for the Run owner; never echo native text.
reason = error.reason_code if isinstance(error, BuilderExecutionPolicyError) else type(error).__name__
logger.warning("Confirmed refresh outcome was not saved: %s", reason)
@classmethod
def get_agent_tool_runtime(
cls,
tenant_id: str,
app_id: str,
agent_tool: AgentToolEntity,
user_id: str | None = None,
invoke_from: InvokeFrom = InvokeFrom.DEBUGGER,
variable_pool: "VariablePool | None" = None,
allow_file_parameters: bool = False,
use_default_for_missing_form_parameters: bool = False,
execution_driver: "WorkflowRunDriver | None" = None,
) -> Tool:
"""
get the agent tool runtime
"""
tool_entity = cls.get_tool_runtime(
provider_type=agent_tool.provider_type,
provider_id=agent_tool.provider_id,
tool_name=agent_tool.tool_name,
tenant_id=tenant_id,
user_id=user_id,
invoke_from=invoke_from,
tool_invoke_from=ToolInvokeFrom.AGENT,
credential_id=agent_tool.credential_id,
execution_driver=execution_driver,
)
runtime_parameters: dict[str, Any] = {}
parameters = tool_entity.get_merged_runtime_parameters()
runtime_parameters = cls._convert_tool_parameters_type(
parameters,
variable_pool,
agent_tool.tool_parameters,
typ="agent",
allow_file_parameters=allow_file_parameters,
use_default_for_missing_form_parameters=use_default_for_missing_form_parameters,
)
# decrypt runtime parameters
encryption_manager = ToolParameterConfigurationManager(
tenant_id=tenant_id,
tool_runtime=tool_entity,
provider_name=agent_tool.provider_id,
provider_type=agent_tool.provider_type,
identity_id=f"AGENT.{app_id}",
)
runtime_parameters = encryption_manager.decrypt_tool_parameters(runtime_parameters)
if tool_entity.runtime is None or tool_entity.runtime.runtime_parameters is None:
raise ValueError("runtime not found or runtime parameters not found")
tool_entity.runtime.runtime_parameters.update(runtime_parameters)
return tool_entity
@classmethod
def get_workflow_tool_runtime(
cls,
tenant_id: str,
app_id: str,
node_id: str,
workflow_tool: WorkflowToolRuntimeSpec,
user_id: str | None = None,
invoke_from: InvokeFrom = InvokeFrom.DEBUGGER,
variable_pool: "VariablePool | None" = None,
*,
confirmed_guard: ConfirmedToolGuard | None = None,
draft_tool_workflow_id: str | None = None,
allow_draft_credential_delegation: bool = False,
builder_expected_credential_fingerprint: str | None = None,
builder_expected_api_binding: str | None = None,
builder_credential_refreshed: Callable[[str], None] | None = None,
execution_driver: "WorkflowRunDriver | None" = None,
workflow_tool_definition: WorkflowTool | None = None,
) -> Tool:
"""
get the workflow tool runtime
``confirmed_guard`` is ``None`` for every ordinary workflow node. A
consented Builder node converts and decrypts no tool configuration: its
literal parameters arrive only with the node's own invocation, where the
guard checks the complete final envelope. ``draft_tool_workflow_id``
comes only from the ordinary native node factory; it never changes the
confirmed early branch or grants authority from graph/client markers.
Delegation is opt-in for ordinary native workflows and remains restricted
to the explicit credential on this exact persisted node.
"""
if confirmed_guard is not None:
return cls.get_tool_runtime(
provider_type=workflow_tool.provider_type,
provider_id=workflow_tool.provider_id,
tool_name=workflow_tool.tool_name,
tenant_id=tenant_id,
user_id=user_id,
invoke_from=invoke_from,
tool_invoke_from=ToolInvokeFrom.WORKFLOW,
credential_id=workflow_tool.credential_id,
confirmed_guard=confirmed_guard,
)
draft_options: _DraftToolRuntimeKwargs = {}
if builder_expected_api_binding is not None:
draft_options["builder_expected_api_binding"] = builder_expected_api_binding
if builder_expected_credential_fingerprint is not None:
draft_options["builder_expected_credential_fingerprint"] = builder_expected_credential_fingerprint
draft_options["builder_credential_refreshed"] = builder_credential_refreshed
if draft_tool_workflow_id is not None:
draft_options["draft_tool_workflow_id"] = draft_tool_workflow_id
draft_options["draft_tool_app_id"] = app_id
draft_options["draft_tool_node_id"] = node_id
draft_options["allow_draft_credential_delegation"] = allow_draft_credential_delegation
if workflow_tool_definition is None:
tool_runtime = cls.get_tool_runtime(
provider_type=workflow_tool.provider_type,
provider_id=workflow_tool.provider_id,
tool_name=workflow_tool.tool_name,
tenant_id=tenant_id,
user_id=user_id,
invoke_from=invoke_from,
tool_invoke_from=ToolInvokeFrom.WORKFLOW,
credential_id=workflow_tool.credential_id,
execution_driver=execution_driver,
**draft_options,
)
else:
tool_runtime = workflow_tool_definition.fork_tool_runtime(
runtime=ToolRuntime(
tenant_id=tenant_id,
user_id=user_id,
credentials={},
invoke_from=invoke_from,
tool_invoke_from=ToolInvokeFrom.WORKFLOW,
)
)
tool_runtime.execution_driver = execution_driver
parameters = tool_runtime.get_merged_runtime_parameters()
runtime_parameters = cls._convert_tool_parameters_type(
parameters, variable_pool, workflow_tool.tool_configurations, typ="workflow"
)
# decrypt runtime parameters
encryption_manager = ToolParameterConfigurationManager(
tenant_id=tenant_id,
tool_runtime=tool_runtime,
provider_name=workflow_tool.provider_id,
provider_type=workflow_tool.provider_type,
identity_id=f"WORKFLOW.{app_id}.{node_id}",
)
if runtime_parameters:
runtime_parameters = encryption_manager.decrypt_tool_parameters(runtime_parameters)
tool_runtime.runtime.runtime_parameters.update(runtime_parameters)
return tool_runtime
@classmethod
def get_tool_runtime_from_plugin(
cls,
tool_type: ToolProviderType,
tenant_id: str,
provider: str,
tool_name: str,
tool_parameters: dict[str, Any],
user_id: str | None = None,
credential_id: str | None = None,
execution_driver: "WorkflowRunDriver | None" = None,
) -> Tool:
"""
get tool runtime from plugin
"""
tool_entity = cls.get_tool_runtime(
provider_type=tool_type,
provider_id=provider,
tool_name=tool_name,
tenant_id=tenant_id,
user_id=user_id,
invoke_from=InvokeFrom.SERVICE_API,
tool_invoke_from=ToolInvokeFrom.PLUGIN,
credential_id=credential_id,
execution_driver=execution_driver,
)
runtime_parameters: dict[str, Any] = {}
parameters = tool_entity.get_merged_runtime_parameters()
for parameter in parameters:
if parameter.form == ToolParameter.ToolParameterForm.FORM:
# save tool parameter to tool entity memory
value = parameter.init_frontend_parameter(tool_parameters.get(parameter.name))
runtime_parameters[parameter.name] = value
tool_entity.runtime.runtime_parameters.update(runtime_parameters)
return tool_entity
@classmethod
def get_hardcoded_provider_icon(cls, provider: str) -> tuple[str, str]:
"""
get the absolute path of the icon of the hardcoded provider
:param provider: the name of the provider
:return: the absolute path of the icon, the mime type of the icon
"""
# get provider
provider_controller = cls.get_hardcoded_provider(provider)
absolute_path = path.join(
provider_controller.provider_directory,
"_assets",
provider_controller.entity.identity.icon,
)
# check if the icon exists
if not path.exists(absolute_path):
raise ToolProviderNotFoundError(f"builtin provider {provider} icon not found")
# get the mime type
mime_type, _ = mimetypes.guess_type(absolute_path)
mime_type = mime_type or "application/octet-stream"
return absolute_path, mime_type
@classmethod
def list_hardcoded_providers(cls):
# use cache first
if cls._builtin_providers_loaded:
yield from list(cls._hardcoded_providers.values())
return
with cls._builtin_provider_lock:
if cls._builtin_providers_loaded:
yield from list(cls._hardcoded_providers.values())
return
yield from cls._list_hardcoded_providers()
@classmethod
def list_plugin_providers(cls, tenant_id: str) -> list[PluginToolProviderController]:
"""
list all the plugin providers
"""
manager = PluginToolManager()
provider_entities = manager.fetch_tool_providers(tenant_id)
return [
PluginToolProviderController(
entity=provider.declaration,
plugin_id=provider.plugin_id,
plugin_unique_identifier=provider.plugin_unique_identifier,
tenant_id=tenant_id,
)
for provider in provider_entities
]
@classmethod
def list_builtin_providers(
cls, tenant_id: str
) -> Generator[BuiltinToolProviderController | PluginToolProviderController]:
"""
list all the builtin providers
"""
yield from cls.list_hardcoded_providers()
# get plugin providers
yield from cls.list_plugin_providers(tenant_id)
@classmethod
def _list_hardcoded_providers(cls) -> Generator[BuiltinToolProviderController]:
"""
list all the builtin providers
"""
for provider_path in listdir(path.join(path.dirname(path.realpath(__file__)), "builtin_tool", "providers")):
if provider_path.startswith("__"):
continue
if path.isdir(path.join(path.dirname(path.realpath(__file__)), "builtin_tool", "providers", provider_path)):
if provider_path.startswith("__"):
continue
# init provider
try:
provider_class = load_single_subclass_from_source(
module_name=f"core.tools.builtin_tool.providers.{provider_path}.{provider_path}",
script_path=path.join(
path.dirname(path.realpath(__file__)),
"builtin_tool",
"providers",
provider_path,
f"{provider_path}.py",
),
parent_type=BuiltinToolProviderController,
)
yield cls._cache_hardcoded_provider(provider_class())
except Exception:
logger.exception("load builtin provider %s", provider_path)
continue
for external_provider_class in cls._external_hardcoded_provider_classes:
try:
yield cls._cache_hardcoded_provider(external_provider_class())
except Exception:
logger.exception("load builtin provider %s", external_provider_class.__module__)
# set builtin providers loaded
cls._builtin_providers_loaded = True
@classmethod
def _cache_hardcoded_provider(cls, provider: BuiltinToolProviderController) -> BuiltinToolProviderController:
cls._hardcoded_providers[provider.entity.identity.name] = provider
for tool in provider.get_tools():
cls._builtin_tools_labels[tool.entity.identity.name] = tool.entity.identity.label
return provider
@classmethod
def register_hardcoded_provider_class(cls, provider_class: type[BuiltinToolProviderController]) -> None:
"""Register a builtin provider owned outside ``core`` from the application composition root."""
with cls._builtin_provider_lock:
if provider_class not in cls._external_hardcoded_provider_classes:
cls._external_hardcoded_provider_classes.append(provider_class)
cls._builtin_providers_loaded = False
@classmethod
def load_hardcoded_providers_cache(cls):
for _ in cls.list_hardcoded_providers():
pass
@classmethod
def clear_hardcoded_providers_cache(cls):
cls._hardcoded_providers = {}
cls._builtin_providers_loaded = False
@classmethod
def get_tool_label(cls, tool_name: str) -> I18nObject | None:
"""
get the tool label
:param tool_name: the name of the tool
:return: the label of the tool
"""
if len(cls._builtin_tools_labels) == 0:
# init the builtin providers
cls.load_hardcoded_providers_cache()
if tool_name not in cls._builtin_tools_labels:
return None
return cls._builtin_tools_labels[tool_name]
@classmethod
def list_default_builtin_providers(cls, tenant_id: str) -> list[BuiltinToolProvider]:
"""
list all the builtin providers
"""
# according to multi credentials, select the one with is_default=True first, then created_at oldest
# for compatibility with old version
if dify_config.SQLALCHEMY_DATABASE_URI_SCHEME == "postgresql":
# PostgreSQL: Use DISTINCT ON
sql = """
SELECT DISTINCT ON (tenant_id, provider) id
FROM tool_builtin_providers
WHERE tenant_id = :tenant_id
ORDER BY tenant_id, provider, is_default DESC, created_at DESC
"""
else:
# MySQL: Use window function to achieve same result
sql = """
SELECT id FROM (
SELECT id,
ROW_NUMBER() OVER (
PARTITION BY tenant_id, provider
ORDER BY is_default DESC, created_at DESC
) as rn
FROM tool_builtin_providers
WHERE tenant_id = :tenant_id
) ranked WHERE rn = 1
"""
with Session(db.engine, autoflush=False) as session:
ids = [row.id for row in session.execute(sa.text(sql), {"tenant_id": tenant_id}).all()]
return list(session.scalars(select(BuiltinToolProvider).where(BuiltinToolProvider.id.in_(ids))))
@classmethod
def list_providers_from_api(
cls, user_id: str, tenant_id: str, typ: ToolProviderTypeApiLiteral
) -> list[ToolProviderApiEntity]:
result_providers: dict[str, ToolProviderApiEntity] = {}
filters = []
if not typ:
filters.extend(["builtin", "api", "workflow", "mcp"])
else:
filters.append(typ)
# Use a single session for all database operations to reduce connection overhead
with Session(db.engine) as session:
if "builtin" in filters:
builtin_providers = list(cls.list_builtin_providers(tenant_id))
# key: provider name, value: provider
db_builtin_providers = {
str(ToolProviderID(provider.provider)): provider
for provider in cls.list_default_builtin_providers(tenant_id)
}
# append builtin providers
for provider in builtin_providers:
# handle include, exclude
if is_filtered(
include_set=dify_config.POSITION_TOOL_INCLUDES_SET,
exclude_set=dify_config.POSITION_TOOL_EXCLUDES_SET,
data=provider,
name_func=lambda x: x.entity.identity.name,
):
continue
user_provider = ToolTransformService.builtin_provider_to_user_provider(
provider_controller=provider,
db_provider=db_builtin_providers.get(provider.entity.identity.name),
decrypt_credentials=False,
)
if isinstance(provider, PluginToolProviderController):
result_providers[f"plugin_provider.{user_provider.name}"] = user_provider
else:
result_providers[f"builtin_provider.{user_provider.name}"] = user_provider
# get db api providers
if "api" in filters:
db_api_providers = session.scalars(
select(ApiToolProvider).where(ApiToolProvider.tenant_id == tenant_id)
).all()
# Batch create controllers
api_provider_controllers: list[ApiProviderControllerItem] = []
for api_provider in db_api_providers:
try:
controller = ToolTransformService.api_provider_to_controller(api_provider)
api_provider_controllers.append({"provider": api_provider, "controller": controller})
except Exception:
# Skip invalid providers but continue processing others
logger.warning("Failed to create controller for API provider %s", api_provider.id)
# Batch get labels for all API providers
if api_provider_controllers:
controllers = cast(
list[ToolProviderController], [item["controller"] for item in api_provider_controllers]
)
labels = ToolLabelManager.get_tools_labels(controllers)
for item in api_provider_controllers:
provider_controller = item["controller"]
db_provider = item["provider"]
provider_labels = labels.get(provider_controller.provider_id, [])
user_provider = ToolTransformService.api_provider_to_user_provider(
provider_controller=provider_controller,
db_provider=db_provider,
decrypt_credentials=False,
labels=provider_labels,
session=db.session(),
)
result_providers[f"api_provider.{user_provider.name}"] = user_provider
if "workflow" in filters:
# get workflow providers
workflow_providers = session.scalars(
select(WorkflowToolProvider).where(WorkflowToolProvider.tenant_id == tenant_id)
).all()
workflow_provider_controllers: list[WorkflowToolProviderController] = []
for workflow_provider in workflow_providers:
try:
workflow_controller: WorkflowToolProviderController = (
ToolTransformService.workflow_provider_to_controller(db_provider=workflow_provider)
)
workflow_provider_controllers.append(workflow_controller)
except Exception:
# app has been deleted
logger.exception("Failed to transform workflow provider %s to controller", workflow_provider.id)
continue
# Batch get labels for workflow providers
if workflow_provider_controllers:
workflow_controllers: list[ToolProviderController] = [
cast(ToolProviderController, controller) for controller in workflow_provider_controllers
]
labels = ToolLabelManager.get_tools_labels(workflow_controllers)
for workflow_provider_controller in workflow_provider_controllers:
provider_labels = labels.get(workflow_provider_controller.provider_id, [])
user_provider = ToolTransformService.workflow_provider_to_user_provider(
provider_controller=workflow_provider_controller,
labels=provider_labels,
)
result_providers[f"workflow_provider.{user_provider.name}"] = user_provider
if "mcp" in filters:
mcp_service = MCPToolManageService(session=session)
mcp_providers = mcp_service.list_providers(tenant_id=tenant_id)
for mcp_provider in mcp_providers:
result_providers[f"mcp_provider.{mcp_provider.name}"] = mcp_provider
return BuiltinToolProviderSort.sort(list(result_providers.values()))
@classmethod
def get_api_provider_controller(
cls, tenant_id: str, provider_id: str, *, expected_binding: str | None = None, action: str = ""
) -> tuple[ApiToolProviderController, dict[str, Any]]:
"""
get the api provider
:param tenant_id: the id of the tenant
:param provider_id: the id of the provider
:return: the provider controller, the credentials
"""
if expected_binding is not None:
# Builder reads one immutable provider snapshot in a short session.
# No metadata transaction survives decryption or provider transport.
with Session(db.engine) as session:
return cls._api_provider_controller_from_session(
session, tenant_id, provider_id, expected_binding=expected_binding, action=action
)
return cls._api_provider_controller_from_session(
db.session(), tenant_id, provider_id, expected_binding=None, action=action
)
@classmethod
def _api_provider_controller_from_session(
cls, session: Session, tenant_id: str, provider_id: str, *, expected_binding: str | None, action: str
) -> tuple[ApiToolProviderController, dict[str, Any]]:
"""Return native metadata and encrypted credentials from this owned session.
Bound calls validate first and use the returned operation snapshot;
callers close the metadata session before decryption or provider I/O.
"""
provider: ApiToolProvider | None = session.scalar(
select(ApiToolProvider)
.where(
ApiToolProvider.id == provider_id,
ApiToolProvider.tenant_id == tenant_id,
)
.limit(1)
)
if provider is None:
raise ToolProviderNotFoundError(f"api provider {provider_id} not found")
selected_bundle = None
if expected_binding is not None:
from core.tools.custom_tool.definition_binding import admit_api_definition
selected_bundle = admit_api_definition(provider, action, expected_binding)
auth_type = ApiProviderAuthType.NONE
provider_auth_type = provider.credentials.get("auth_type")
if provider_auth_type in ("api_key_header", "api_key"): # backward compatibility
auth_type = ApiProviderAuthType.API_KEY_HEADER
elif provider_auth_type == "api_key_query":
auth_type = ApiProviderAuthType.API_KEY_QUERY
controller = ApiToolProviderController.from_db(
provider,
auth_type,
session=session,
)
controller.load_bundled_tools([selected_bundle] if selected_bundle is not None else provider.tools)
return controller, provider.credentials
@classmethod
def get_mcp_provider_controller(cls, tenant_id: str, provider_id: str) -> MCPToolProviderController:
"""
get the api provider
:param tenant_id: the id of the tenant
:param provider_id: the persisted reference of the provider, normally its
server identifier, or the primary key for graphs written before that
convention
:return: the provider controller, the credentials
"""
with Session(db.engine) as session:
mcp_service = MCPToolManageService(session=session)
try:
provider = mcp_service.get_provider_by_persisted_reference(
id_or_server_identifier=provider_id, tenant_id=tenant_id
)
except ValueError as caught_error:
raise ToolProviderNotFoundError(f"mcp provider {provider_id} not found") from caught_error
controller = MCPToolProviderController.from_db(provider)
return controller
@classmethod
def user_get_api_provider(cls, provider: str, tenant_id: str, mask: bool = True):
"""
get api provider
"""
provider_name = provider
provider_obj: ApiToolProvider | None = db.session.scalar(
select(ApiToolProvider)
.where(
ApiToolProvider.tenant_id == tenant_id,
ApiToolProvider.name == provider,
)
.limit(1)
)
if provider_obj is None:
raise ValueError(f"you have not added provider {provider_name}")
try:
credentials = _credentials_adapter.validate_json(provider_obj.credentials_str) or {}
except Exception:
credentials = {}
# package tool provider controller
auth_type = ApiProviderAuthType.NONE
credentials_auth_type = credentials.get("auth_type")
if credentials_auth_type in ("api_key_header", "api_key"): # backward compatibility
auth_type = ApiProviderAuthType.API_KEY_HEADER
elif credentials_auth_type == "api_key_query":
auth_type = ApiProviderAuthType.API_KEY_QUERY
controller = ApiToolProviderController.from_db(
provider_obj,
auth_type,
session=db.session(),
)
# init tool configuration
encrypter, _ = create_tool_provider_encrypter(
tenant_id=tenant_id,
controller=controller,
)
if mask:
masked_credentials = encrypter.mask_plugin_credentials(encrypter.decrypt(credentials))
else:
masked_credentials = encrypter.decrypt(credentials)
try:
icon = emoji_icon_adapter.validate_json(provider_obj.icon)
except Exception:
icon = {"background": "#252525", "content": "\ud83d\ude01"}
# add tool labels
labels = ToolLabelManager.get_tool_labels(controller)
schema_type = provider_obj.schema_type
schema_type_value = schema_type.value if isinstance(schema_type, ApiProviderSchemaType) else schema_type
return {
"schema_type": schema_type_value,
"schema": provider_obj.schema,
"tools": [tool.model_dump(mode="json") for tool in provider_obj.tools],
"icon": icon,
"description": provider_obj.description,
"credentials": masked_credentials,
"privacy_policy": provider_obj.privacy_policy,
"custom_disclaimer": provider_obj.custom_disclaimer,
"labels": labels,
}
@classmethod
def generate_builtin_tool_icon_url(cls, provider_id: str) -> str:
return str(
URL(dify_config.CONSOLE_API_URL or "/")
/ "console"
/ "api"
/ "workspaces"
/ "current"
/ "tool-provider"
/ "builtin"
/ provider_id
/ "icon"
)
@classmethod
def generate_plugin_tool_icon_url(cls, tenant_id: str, filename: str) -> str:
return str(
URL(dify_config.CONSOLE_API_URL or "/")
/ "console"
/ "api"
/ "workspaces"
/ "current"
/ "plugin"
/ "icon"
% {"tenant_id": tenant_id, "filename": filename}
)
@classmethod
def generate_workflow_tool_icon_url(cls, tenant_id: str, provider_id: str) -> EmojiIconDict:
try:
# Use a short-lived session to avoid holding a database transaction
# during long-running nested workflow execution.
# Fixes: idle in transaction when Workflow Tool runs (#36902)
with Session(db.engine, expire_on_commit=False) as session:
workflow_provider: WorkflowToolProvider | None = session.scalar(
select(WorkflowToolProvider)
.where(WorkflowToolProvider.tenant_id == tenant_id, WorkflowToolProvider.id == provider_id)
.limit(1)
)
if workflow_provider is None:
raise ToolProviderNotFoundError(f"workflow provider {provider_id} not found")
icon = emoji_icon_adapter.validate_json(workflow_provider.icon)
return icon
except Exception:
return {"background": "#252525", "content": "\ud83d\ude01"}
@classmethod
def generate_api_tool_icon_url(cls, tenant_id: str, provider_id: str) -> EmojiIconDict:
try:
# Use a short-lived session to avoid holding a database transaction
# during long-running tool execution.
with Session(db.engine, expire_on_commit=False) as session:
api_provider: ApiToolProvider | None = session.scalar(
select(ApiToolProvider)
.where(ApiToolProvider.tenant_id == tenant_id, ApiToolProvider.id == provider_id)
.limit(1)
)
if api_provider is None:
raise ToolProviderNotFoundError(f"api provider {provider_id} not found")
icon = emoji_icon_adapter.validate_json(api_provider.icon)
return icon
except Exception:
return {"background": "#252525", "content": "\ud83d\ude01"}
@classmethod
def generate_mcp_tool_icon_url(cls, tenant_id: str, provider_id: str) -> EmojiIconDict | str:
try:
with Session(db.engine) as session:
mcp_service = MCPToolManageService(session=session)
try:
mcp_provider = mcp_service.get_provider_entity_by_persisted_reference(
id_or_server_identifier=provider_id, tenant_id=tenant_id
)
return cast(EmojiIconDict | str, mcp_provider.provider_icon)
except ValueError as caught_error:
raise ToolProviderNotFoundError(f"mcp provider {provider_id} not found") from caught_error
except Exception:
return {"background": "#252525", "content": "\ud83d\ude01"}
@classmethod
def get_tool_icon(
cls,
tenant_id: str,
provider_type: ToolProviderType,
provider_id: str,
) -> str | EmojiIconDict:
"""
get the tool icon
:param tenant_id: the id of the tenant
:param provider_type: the type of the provider
:param provider_id: the id of the provider
:return:
"""
match provider_type:
case ToolProviderType.BUILT_IN:
provider = ToolManager.get_builtin_provider(provider_id, tenant_id)
if isinstance(provider, PluginToolProviderController):
try:
return cls.generate_plugin_tool_icon_url(tenant_id, provider.entity.identity.icon)
except Exception:
return {"background": "#252525", "content": "\ud83d\ude01"}
return cls.generate_builtin_tool_icon_url(provider_id)
case ToolProviderType.API:
return cls.generate_api_tool_icon_url(tenant_id, provider_id)
case ToolProviderType.WORKFLOW:
return cls.generate_workflow_tool_icon_url(tenant_id, provider_id)
case ToolProviderType.PLUGIN:
provider = ToolManager.get_plugin_provider(provider_id, tenant_id)
try:
return cls.generate_plugin_tool_icon_url(tenant_id, provider.entity.identity.icon)
except Exception:
return {"background": "#252525", "content": "\ud83d\ude01"}
case ToolProviderType.MCP:
return cls.generate_mcp_tool_icon_url(tenant_id, provider_id)
case ToolProviderType.APP | ToolProviderType.DATASET_RETRIEVAL:
raise ValueError(f"provider type {provider_type} not found")
case _:
raise ValueError(f"provider type {provider_type} not found")
@classmethod
def _convert_tool_parameters_type(
cls,
parameters: list[ToolParameter],
variable_pool: "VariablePool | None",
tool_configurations: Mapping[str, Any],
typ: Literal["agent", "workflow", "tool"] = "workflow",
allow_file_parameters: bool = False,
use_default_for_missing_form_parameters: bool = False,
) -> dict[str, Any]:
"""
Convert tool parameters type
"""
from graphon.nodes.tool.entities import ToolNodeData
from graphon.nodes.tool.exc import ToolParameterError
runtime_parameters: dict[str, Any] = {}
for parameter in parameters:
if (
parameter.type
in {
ToolParameter.ToolParameterType.SYSTEM_FILES,
ToolParameter.ToolParameterType.FILE,
ToolParameter.ToolParameterType.FILES,
}
and parameter.required
and typ == "agent"
and not allow_file_parameters
):
raise ValueError(f"file type parameter {parameter.name} not supported in agent")
# save tool parameter to tool entity memory
if parameter.form == ToolParameter.ToolParameterForm.FORM:
if variable_pool:
config = tool_configurations.get(parameter.name, {})
selector_value = cls._extract_runtime_selector_value(parameter, config)
if selector_value is not None:
# Selector parameters carry structured dictionaries, not scalar ToolInput values.
runtime_parameters[parameter.name] = selector_value
continue
if not (config and isinstance(config, dict) and config.get("value") is not None):
continue
tool_input = ToolNodeData.ToolInput.model_validate(tool_configurations.get(parameter.name, {}))
if tool_input.type == "variable":
variable_selector = tool_input.value
if not isinstance(variable_selector, list) or not all(
isinstance(selector_part, str) for selector_part in variable_selector
):
raise ToolParameterError("Variable tool input must be a variable selector")
variable = variable_pool.get(variable_selector)
if variable is None:
raise ToolParameterError(f"Variable {tool_input.value} does not exist")
parameter_value = variable.value
elif tool_input.type == "constant":
parameter_value = tool_input.value
elif tool_input.type == "mixed":
segment_group = convert_template(variable_pool, str(tool_input.value))
parameter_value = segment_group.text
else:
raise ToolParameterError(f"Unknown tool input type '{tool_input.type}'")
runtime_parameters[parameter.name] = parameter_value
else:
parameter_value = tool_configurations.get(parameter.name)
if use_default_for_missing_form_parameters and parameter_value is None:
if parameter.default is not None:
parameter_value = parameter.default
elif (
parameter.required
and parameter.type == ToolParameter.ToolParameterType.SELECT
and parameter.options
):
parameter_value = parameter.options[0].value
else:
continue
value = parameter.init_frontend_parameter(parameter_value)
runtime_parameters[parameter.name] = value
return runtime_parameters
@classmethod
def _extract_runtime_selector_value(cls, parameter: ToolParameter, config: Any) -> dict[str, Any] | None:
if parameter.type not in {
ToolParameter.ToolParameterType.MODEL_SELECTOR,
ToolParameter.ToolParameterType.APP_SELECTOR,
}:
return None
if not isinstance(config, dict):
return None
input_value = config.get("value")
if isinstance(input_value, dict) and cls._is_selector_value(parameter, input_value):
return cast("dict[str, Any]", parameter.init_frontend_parameter(input_value))
if cls._is_selector_value(parameter, config):
selector_value = dict(config)
selector_value.pop("type", None)
selector_value.pop("value", None)
return cast("dict[str, Any]", parameter.init_frontend_parameter(selector_value))
return None
@classmethod
def _is_selector_value(cls, parameter: ToolParameter, value: Mapping[str, Any]) -> bool:
if parameter.type == ToolParameter.ToolParameterType.MODEL_SELECTOR:
return (
isinstance(value.get("provider"), str)
and isinstance(value.get("model"), str)
and isinstance(value.get("model_type"), str)
)
if parameter.type == ToolParameter.ToolParameterType.APP_SELECTOR:
return isinstance(value.get("app_id"), str)
return False
ToolManager.load_hardcoded_providers_cache()