mirror of
https://github.com/langgenius/dify.git
synced 2026-11-11 22:02:23 +08:00
162 lines
6.1 KiB
Python
162 lines
6.1 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
from collections.abc import Generator, Mapping
|
|
from typing import Any, override
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from core.dify_builder.execution_policy import BuilderExecutionPolicyError
|
|
from core.dify_builder.real_call_consent import ConfirmedToolGuard
|
|
from core.plugin.impl.tool import PluginToolManager
|
|
from core.plugin.utils.converter import convert_parameters_to_plugin_format
|
|
from core.tools.__base.tool import Tool
|
|
from core.tools.__base.tool_runtime import ToolRuntime
|
|
from core.tools.entities.tool_entities import ToolEntity, ToolInvokeMessage, ToolParameter, ToolProviderType
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# A confirmed call forwards only data messages: anything that could trigger a
|
|
# secondary download or file write is refused before file transformation.
|
|
_CONFIRMED_MESSAGE_TYPES = frozenset(
|
|
{ToolInvokeMessage.MessageType.TEXT, ToolInvokeMessage.MessageType.JSON, ToolInvokeMessage.MessageType.VARIABLE}
|
|
)
|
|
|
|
|
|
class PluginTool(Tool):
|
|
def __init__(
|
|
self,
|
|
entity: ToolEntity,
|
|
runtime: ToolRuntime,
|
|
tenant_id: str,
|
|
icon: str,
|
|
plugin_unique_identifier: str,
|
|
*,
|
|
confirmed_guard: ConfirmedToolGuard | None = None,
|
|
):
|
|
super().__init__(entity, runtime)
|
|
self.tenant_id = tenant_id
|
|
self.icon = icon
|
|
self.plugin_unique_identifier = plugin_unique_identifier
|
|
self.runtime_parameters: list[ToolParameter] | None = None
|
|
# ``None`` for every ordinary tool. A confirmed Builder call carries its
|
|
# in-process consent callbacks here, never in ``ToolRuntime`` or any
|
|
# serialized payload.
|
|
self.confirmed_guard = confirmed_guard
|
|
|
|
@override
|
|
def tool_provider_type(self) -> ToolProviderType:
|
|
return ToolProviderType.PLUGIN
|
|
|
|
@override
|
|
def _invoke(
|
|
self,
|
|
session: Session,
|
|
user_id: str,
|
|
tool_parameters: dict[str, Any],
|
|
conversation_id: str | None = None,
|
|
app_id: str | None = None,
|
|
message_id: str | None = None,
|
|
) -> Generator[ToolInvokeMessage, None, None]:
|
|
manager = PluginToolManager()
|
|
|
|
tool_parameters = convert_parameters_to_plugin_format(tool_parameters)
|
|
|
|
def dispatch(confirmed_headers: Mapping[str, str] | None) -> Generator[ToolInvokeMessage, None, None]:
|
|
return manager.invoke(
|
|
tenant_id=self.tenant_id,
|
|
user_id=user_id,
|
|
tool_provider=self.entity.identity.provider,
|
|
tool_name=self.entity.identity.name,
|
|
credentials=self.runtime.credentials,
|
|
credential_type=self.runtime.credential_type,
|
|
tool_parameters=tool_parameters,
|
|
conversation_id=conversation_id,
|
|
app_id=app_id,
|
|
message_id=message_id,
|
|
confirmed_dispatch_headers=confirmed_headers,
|
|
)
|
|
|
|
guard = self.confirmed_guard
|
|
if guard is None:
|
|
yield from dispatch(None)
|
|
return
|
|
|
|
# The complete converted envelope is compared and the business phase
|
|
# spent before the single native dispatch; a refusal sends nothing.
|
|
guard.before_invoke(tool_parameters)
|
|
complete = False
|
|
try:
|
|
messages = dispatch(guard.dispatch_headers())
|
|
try:
|
|
for message in messages:
|
|
self._observe_confirmed(guard, message)
|
|
yield message
|
|
finally:
|
|
messages.close()
|
|
complete = True
|
|
finally:
|
|
# Exactly once per spent phase: cancellation, timeout, transport loss
|
|
# or a refused message leave the outcome unknown, never failed.
|
|
self._finish_confirmed(guard, complete=complete)
|
|
|
|
@staticmethod
|
|
def _observe_confirmed(guard: ConfirmedToolGuard, message: ToolInvokeMessage) -> None:
|
|
if message.type not in _CONFIRMED_MESSAGE_TYPES:
|
|
raise BuilderExecutionPolicyError("unsupported_tool_output")
|
|
if message.type == ToolInvokeMessage.MessageType.JSON:
|
|
payload = message.message
|
|
if not isinstance(payload, ToolInvokeMessage.JsonMessage) or not isinstance(payload.json_object, dict):
|
|
raise BuilderExecutionPolicyError("unsupported_tool_output")
|
|
guard.observe_json(payload.json_object)
|
|
|
|
@staticmethod
|
|
def _finish_confirmed(guard: ConfirmedToolGuard, *, complete: bool) -> None:
|
|
try:
|
|
guard.finish_stream(complete=complete, declared_failure=False)
|
|
except Exception as error:
|
|
# The spent phase stays unresolved (unknown) for the Run owner; never echo native text.
|
|
reason = error.reason_code if isinstance(error, BuilderExecutionPolicyError) else type(error).__name__
|
|
logger.warning("Confirmed tool outcome was not recorded: %s", reason)
|
|
|
|
@override
|
|
def fork_tool_runtime(self, runtime: ToolRuntime) -> PluginTool:
|
|
return PluginTool(
|
|
entity=self.entity,
|
|
runtime=runtime,
|
|
tenant_id=self.tenant_id,
|
|
icon=self.icon,
|
|
plugin_unique_identifier=self.plugin_unique_identifier,
|
|
confirmed_guard=self.confirmed_guard,
|
|
)
|
|
|
|
@override
|
|
def get_runtime_parameters(
|
|
self,
|
|
conversation_id: str | None = None,
|
|
app_id: str | None = None,
|
|
message_id: str | None = None,
|
|
) -> list[ToolParameter]:
|
|
"""
|
|
get the runtime parameters
|
|
"""
|
|
if not self.entity.has_runtime_parameters:
|
|
return self.entity.parameters
|
|
|
|
if self.runtime_parameters is not None:
|
|
return self.runtime_parameters
|
|
|
|
manager = PluginToolManager()
|
|
self.runtime_parameters = manager.get_runtime_parameters(
|
|
tenant_id=self.tenant_id,
|
|
user_id="",
|
|
provider=self.entity.identity.provider,
|
|
tool=self.entity.identity.name,
|
|
credentials=self.runtime.credentials,
|
|
conversation_id=conversation_id,
|
|
app_id=app_id,
|
|
message_id=message_id,
|
|
)
|
|
|
|
return self.runtime_parameters
|