from abc import ABC, abstractmethod from collections.abc import Generator from copy import deepcopy from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from models.model import File from core.model_runtime.entities.message_entities import PromptMessageTool from core.tools.__base.tool_runtime import ToolRuntime from core.tools.entities.tool_entities import ( ToolEntity, ToolInvokeMessage, ToolParameter, ToolProviderType, ) class Tool(ABC): """ The base class of a tool """ def __init__(self, entity: ToolEntity, runtime: ToolRuntime): self.entity = entity self.runtime = runtime def fork_tool_runtime(self, runtime: ToolRuntime) -> "Tool": """ fork a new tool with metadata :return: the new tool """ return self.__class__( entity=self.entity.model_copy(), runtime=runtime, ) @abstractmethod def tool_provider_type(self) -> ToolProviderType: """ get the tool provider type :return: the tool provider type """ def invoke( self, 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]: if self.runtime and self.runtime.runtime_parameters: tool_parameters.update(self.runtime.runtime_parameters) # try parse tool parameters into the correct type tool_parameters = self._transform_tool_parameters_type(tool_parameters) result = self._invoke( user_id=user_id, tool_parameters=tool_parameters, conversation_id=conversation_id, app_id=app_id, message_id=message_id, ) if isinstance(result, ToolInvokeMessage): def single_generator() -> Generator[ToolInvokeMessage, None, None]: yield result return single_generator() elif isinstance(result, list): def generator() -> Generator[ToolInvokeMessage, None, None]: yield from result return generator() else: return result def _transform_tool_parameters_type(self, tool_parameters: dict[str, Any]) -> dict[str, Any]: """ Transform tool parameters type """ # Temp fix for the issue that the tool parameters will be converted to empty while validating the credentials result = deepcopy(tool_parameters) for parameter in self.entity.parameters or []: if parameter.name in tool_parameters: result[parameter.name] = parameter.type.cast_value(tool_parameters[parameter.name]) return result @abstractmethod def _invoke( self, user_id: str, tool_parameters: dict[str, Any], conversation_id: str | None = None, app_id: str | None = None, message_id: str | None = None, ) -> ToolInvokeMessage | list[ToolInvokeMessage] | Generator[ToolInvokeMessage, None, None]: pass 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 interface for developer to dynamic change the parameters of a tool depends on the variables pool :return: the runtime parameters """ return self.entity.parameters def get_merged_runtime_parameters( self, conversation_id: str | None = None, app_id: str | None = None, message_id: str | None = None, ) -> list[ToolParameter]: """ get merged runtime parameters :return: merged runtime parameters """ parameters = self.entity.parameters parameters = parameters.copy() user_parameters = self.get_runtime_parameters() or [] user_parameters = user_parameters.copy() # override parameters for parameter in user_parameters: # check if parameter in tool parameters for tool_parameter in parameters: if tool_parameter.name == parameter.name: # override parameter tool_parameter.type = parameter.type tool_parameter.form = parameter.form tool_parameter.required = parameter.required tool_parameter.default = parameter.default tool_parameter.options = parameter.options tool_parameter.llm_description = parameter.llm_description break else: # add new parameter parameters.append(parameter) return parameters def to_prompt_message_tool(self) -> PromptMessageTool: message_tool = PromptMessageTool( name=self.entity.identity.name, description=self.entity.description.llm if self.entity.description else "", parameters={ "type": "object", "properties": {}, "required": [], }, ) parameters = self.get_merged_runtime_parameters() for parameter in parameters: if parameter.form != ToolParameter.ToolParameterForm.LLM: continue parameter_type = parameter.type.as_normal_type() if parameter.type in { ToolParameter.ToolParameterType.SYSTEM_FILES, ToolParameter.ToolParameterType.FILE, ToolParameter.ToolParameterType.FILES, }: # Determine the description based on parameter type if parameter.type == ToolParameter.ToolParameterType.FILE: file_format_desc = " Input the file id with format: [File: file_id]." else: file_format_desc = "Input the file id with format: [Files: file_id1, file_id2, ...]. " message_tool.parameters["properties"][parameter.name] = { "type": "string", "description": (parameter.llm_description or "") + file_format_desc, } continue enum = [] if parameter.type == ToolParameter.ToolParameterType.SELECT: enum = [option.value for option in parameter.options] if parameter.options else [] message_tool.parameters["properties"][parameter.name] = ( { "type": parameter_type, "description": parameter.llm_description or "", } if parameter.input_schema is None else parameter.input_schema ) if len(enum) > 0: message_tool.parameters["properties"][parameter.name]["enum"] = enum if parameter.required: message_tool.parameters["required"].append(parameter.name) return message_tool def create_image_message( self, image: str, ) -> ToolInvokeMessage: """ create an image message :param image: the url of the image :return: the image message """ return ToolInvokeMessage( type=ToolInvokeMessage.MessageType.IMAGE, message=ToolInvokeMessage.TextMessage(text=image) ) def create_file_message(self, file: "File") -> ToolInvokeMessage: return ToolInvokeMessage( type=ToolInvokeMessage.MessageType.FILE, message=ToolInvokeMessage.FileMessage(), meta={"file": file}, ) def create_link_message(self, link: str) -> ToolInvokeMessage: """ create a link message :param link: the url of the link :return: the link message """ return ToolInvokeMessage( type=ToolInvokeMessage.MessageType.LINK, message=ToolInvokeMessage.TextMessage(text=link) ) def create_text_message(self, text: str) -> ToolInvokeMessage: """ create a text message :param text: the text :return: the text message """ return ToolInvokeMessage( type=ToolInvokeMessage.MessageType.TEXT, message=ToolInvokeMessage.TextMessage(text=text), ) def create_blob_message(self, blob: bytes, meta: dict | None = None) -> ToolInvokeMessage: """ create a blob message :param blob: the blob :param meta: the meta info of blob object :return: the blob message """ return ToolInvokeMessage( type=ToolInvokeMessage.MessageType.BLOB, message=ToolInvokeMessage.BlobMessage(blob=blob), meta=meta, ) def create_json_message(self, object: dict, suppress_output: bool = False) -> ToolInvokeMessage: """ create a json message """ return ToolInvokeMessage( type=ToolInvokeMessage.MessageType.JSON, message=ToolInvokeMessage.JsonMessage(json_object=object, suppress_output=suppress_output), ) def create_variable_message( self, variable_name: str, variable_value: Any, stream: bool = False ) -> ToolInvokeMessage: """ create a variable message """ return ToolInvokeMessage( type=ToolInvokeMessage.MessageType.VARIABLE, message=ToolInvokeMessage.VariableMessage( variable_name=variable_name, variable_value=variable_value, stream=stream ), )