tool_manager.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600
  1. import json
  2. import logging
  3. import mimetypes
  4. from collections.abc import Generator
  5. from os import listdir, path
  6. from threading import Lock
  7. from typing import Any, Union
  8. from configs import dify_config
  9. from core.agent.entities import AgentToolEntity
  10. from core.app.entities.app_invoke_entities import InvokeFrom
  11. from core.helper.module_import_helper import load_single_subclass_from_source
  12. from core.helper.position_helper import is_filtered
  13. from core.model_runtime.utils.encoders import jsonable_encoder
  14. from core.tools.entities.api_entities import UserToolProvider, UserToolProviderTypeLiteral
  15. from core.tools.entities.common_entities import I18nObject
  16. from core.tools.entities.tool_entities import ApiProviderAuthType, ToolInvokeFrom, ToolParameter
  17. from core.tools.errors import ToolProviderNotFoundError
  18. from core.tools.provider.api_tool_provider import ApiToolProviderController
  19. from core.tools.provider.builtin._positions import BuiltinToolProviderSort
  20. from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController
  21. from core.tools.tool.api_tool import ApiTool
  22. from core.tools.tool.builtin_tool import BuiltinTool
  23. from core.tools.tool.tool import Tool
  24. from core.tools.tool_label_manager import ToolLabelManager
  25. from core.tools.utils.configuration import ToolConfigurationManager, ToolParameterConfigurationManager
  26. from core.tools.utils.tool_parameter_converter import ToolParameterConverter
  27. from extensions.ext_database import db
  28. from models.tools import ApiToolProvider, BuiltinToolProvider, WorkflowToolProvider
  29. from services.tools.tools_transform_service import ToolTransformService
  30. logger = logging.getLogger(__name__)
  31. class ToolManager:
  32. _builtin_provider_lock = Lock()
  33. _builtin_providers = {}
  34. _builtin_providers_loaded = False
  35. _builtin_tools_labels = {}
  36. @classmethod
  37. def get_builtin_provider(cls, provider: str) -> BuiltinToolProviderController:
  38. """
  39. get the builtin provider
  40. :param provider: the name of the provider
  41. :return: the provider
  42. """
  43. if len(cls._builtin_providers) == 0:
  44. # init the builtin providers
  45. cls.load_builtin_providers_cache()
  46. if provider not in cls._builtin_providers:
  47. raise ToolProviderNotFoundError(f'builtin provider {provider} not found')
  48. return cls._builtin_providers[provider]
  49. @classmethod
  50. def get_builtin_tool(cls, provider: str, tool_name: str) -> BuiltinTool:
  51. """
  52. get the builtin tool
  53. :param provider: the name of the provider
  54. :param tool_name: the name of the tool
  55. :return: the provider, the tool
  56. """
  57. provider_controller = cls.get_builtin_provider(provider)
  58. tool = provider_controller.get_tool(tool_name)
  59. return tool
  60. @classmethod
  61. def get_tool(cls, provider_type: str, provider_id: str, tool_name: str, tenant_id: str = None) \
  62. -> Union[BuiltinTool, ApiTool]:
  63. """
  64. get the tool
  65. :param provider_type: the type of the provider
  66. :param provider_name: the name of the provider
  67. :param tool_name: the name of the tool
  68. :return: the tool
  69. """
  70. if provider_type == 'builtin':
  71. return cls.get_builtin_tool(provider_id, tool_name)
  72. elif provider_type == 'api':
  73. if tenant_id is None:
  74. raise ValueError('tenant id is required for api provider')
  75. api_provider, _ = cls.get_api_provider_controller(tenant_id, provider_id)
  76. return api_provider.get_tool(tool_name)
  77. elif provider_type == 'app':
  78. raise NotImplementedError('app provider not implemented')
  79. else:
  80. raise ToolProviderNotFoundError(f'provider type {provider_type} not found')
  81. @classmethod
  82. def get_tool_runtime(cls, provider_type: str,
  83. provider_id: str,
  84. tool_name: str,
  85. tenant_id: str,
  86. invoke_from: InvokeFrom = InvokeFrom.DEBUGGER,
  87. tool_invoke_from: ToolInvokeFrom = ToolInvokeFrom.AGENT) \
  88. -> Union[BuiltinTool, ApiTool]:
  89. """
  90. get the tool runtime
  91. :param provider_type: the type of the provider
  92. :param provider_name: the name of the provider
  93. :param tool_name: the name of the tool
  94. :return: the tool
  95. """
  96. if provider_type == 'builtin':
  97. builtin_tool = cls.get_builtin_tool(provider_id, tool_name)
  98. # check if the builtin tool need credentials
  99. provider_controller = cls.get_builtin_provider(provider_id)
  100. if not provider_controller.need_credentials:
  101. return builtin_tool.fork_tool_runtime(runtime={
  102. 'tenant_id': tenant_id,
  103. 'credentials': {},
  104. 'invoke_from': invoke_from,
  105. 'tool_invoke_from': tool_invoke_from,
  106. })
  107. # get credentials
  108. builtin_provider: BuiltinToolProvider = db.session.query(BuiltinToolProvider).filter(
  109. BuiltinToolProvider.tenant_id == tenant_id,
  110. BuiltinToolProvider.provider == provider_id,
  111. ).first()
  112. if builtin_provider is None:
  113. raise ToolProviderNotFoundError(f'builtin provider {provider_id} not found')
  114. # decrypt the credentials
  115. credentials = builtin_provider.credentials
  116. controller = cls.get_builtin_provider(provider_id)
  117. tool_configuration = ToolConfigurationManager(tenant_id=tenant_id, provider_controller=controller)
  118. decrypted_credentials = tool_configuration.decrypt_tool_credentials(credentials)
  119. return builtin_tool.fork_tool_runtime(runtime={
  120. 'tenant_id': tenant_id,
  121. 'credentials': decrypted_credentials,
  122. 'runtime_parameters': {},
  123. 'invoke_from': invoke_from,
  124. 'tool_invoke_from': tool_invoke_from,
  125. })
  126. elif provider_type == 'api':
  127. if tenant_id is None:
  128. raise ValueError('tenant id is required for api provider')
  129. api_provider, credentials = cls.get_api_provider_controller(tenant_id, provider_id)
  130. # decrypt the credentials
  131. tool_configuration = ToolConfigurationManager(tenant_id=tenant_id, provider_controller=api_provider)
  132. decrypted_credentials = tool_configuration.decrypt_tool_credentials(credentials)
  133. return api_provider.get_tool(tool_name).fork_tool_runtime(runtime={
  134. 'tenant_id': tenant_id,
  135. 'credentials': decrypted_credentials,
  136. 'invoke_from': invoke_from,
  137. 'tool_invoke_from': tool_invoke_from,
  138. })
  139. elif provider_type == 'workflow':
  140. workflow_provider = db.session.query(WorkflowToolProvider).filter(
  141. WorkflowToolProvider.tenant_id == tenant_id,
  142. WorkflowToolProvider.id == provider_id
  143. ).first()
  144. if workflow_provider is None:
  145. raise ToolProviderNotFoundError(f'workflow provider {provider_id} not found')
  146. controller = ToolTransformService.workflow_provider_to_controller(
  147. db_provider=workflow_provider
  148. )
  149. return controller.get_tools(user_id=None, tenant_id=workflow_provider.tenant_id)[0].fork_tool_runtime(runtime={
  150. 'tenant_id': tenant_id,
  151. 'credentials': {},
  152. 'invoke_from': invoke_from,
  153. 'tool_invoke_from': tool_invoke_from,
  154. })
  155. elif provider_type == 'app':
  156. raise NotImplementedError('app provider not implemented')
  157. else:
  158. raise ToolProviderNotFoundError(f'provider type {provider_type} not found')
  159. @classmethod
  160. def _init_runtime_parameter(cls, parameter_rule: ToolParameter, parameters: dict) -> Union[str, int, float, bool]:
  161. """
  162. init runtime parameter
  163. """
  164. parameter_value = parameters.get(parameter_rule.name)
  165. if not parameter_value and parameter_value != 0:
  166. # get default value
  167. parameter_value = parameter_rule.default
  168. if not parameter_value and parameter_rule.required:
  169. raise ValueError(f"tool parameter {parameter_rule.name} not found in tool config")
  170. if parameter_rule.type == ToolParameter.ToolParameterType.SELECT:
  171. # check if tool_parameter_config in options
  172. options = [x.value for x in parameter_rule.options]
  173. if parameter_value is not None and parameter_value not in options:
  174. raise ValueError(
  175. f"tool parameter {parameter_rule.name} value {parameter_value} not in options {options}")
  176. return ToolParameterConverter.cast_parameter_by_type(parameter_value, parameter_rule.type)
  177. @classmethod
  178. def get_agent_tool_runtime(cls, tenant_id: str, app_id: str, agent_tool: AgentToolEntity, invoke_from: InvokeFrom = InvokeFrom.DEBUGGER) -> Tool:
  179. """
  180. get the agent tool runtime
  181. """
  182. tool_entity = cls.get_tool_runtime(
  183. provider_type=agent_tool.provider_type,
  184. provider_id=agent_tool.provider_id,
  185. tool_name=agent_tool.tool_name,
  186. tenant_id=tenant_id,
  187. invoke_from=invoke_from,
  188. tool_invoke_from=ToolInvokeFrom.AGENT
  189. )
  190. runtime_parameters = {}
  191. parameters = tool_entity.get_all_runtime_parameters()
  192. for parameter in parameters:
  193. # check file types
  194. if parameter.type == ToolParameter.ToolParameterType.FILE:
  195. raise ValueError(f"file type parameter {parameter.name} not supported in agent")
  196. if parameter.form == ToolParameter.ToolParameterForm.FORM:
  197. # save tool parameter to tool entity memory
  198. value = cls._init_runtime_parameter(parameter, agent_tool.tool_parameters)
  199. runtime_parameters[parameter.name] = value
  200. # decrypt runtime parameters
  201. encryption_manager = ToolParameterConfigurationManager(
  202. tenant_id=tenant_id,
  203. tool_runtime=tool_entity,
  204. provider_name=agent_tool.provider_id,
  205. provider_type=agent_tool.provider_type,
  206. identity_id=f'AGENT.{app_id}'
  207. )
  208. runtime_parameters = encryption_manager.decrypt_tool_parameters(runtime_parameters)
  209. tool_entity.runtime.runtime_parameters.update(runtime_parameters)
  210. return tool_entity
  211. @classmethod
  212. def get_workflow_tool_runtime(cls, tenant_id: str, app_id: str, node_id: str, workflow_tool: "ToolEntity", invoke_from: InvokeFrom = InvokeFrom.DEBUGGER) -> Tool:
  213. """
  214. get the workflow tool runtime
  215. """
  216. tool_entity = cls.get_tool_runtime(
  217. provider_type=workflow_tool.provider_type,
  218. provider_id=workflow_tool.provider_id,
  219. tool_name=workflow_tool.tool_name,
  220. tenant_id=tenant_id,
  221. invoke_from=invoke_from,
  222. tool_invoke_from=ToolInvokeFrom.WORKFLOW
  223. )
  224. runtime_parameters = {}
  225. parameters = tool_entity.get_all_runtime_parameters()
  226. for parameter in parameters:
  227. # save tool parameter to tool entity memory
  228. if parameter.form == ToolParameter.ToolParameterForm.FORM:
  229. value = cls._init_runtime_parameter(parameter, workflow_tool.tool_configurations)
  230. runtime_parameters[parameter.name] = value
  231. # decrypt runtime parameters
  232. encryption_manager = ToolParameterConfigurationManager(
  233. tenant_id=tenant_id,
  234. tool_runtime=tool_entity,
  235. provider_name=workflow_tool.provider_id,
  236. provider_type=workflow_tool.provider_type,
  237. identity_id=f'WORKFLOW.{app_id}.{node_id}'
  238. )
  239. if runtime_parameters:
  240. runtime_parameters = encryption_manager.decrypt_tool_parameters(runtime_parameters)
  241. tool_entity.runtime.runtime_parameters.update(runtime_parameters)
  242. return tool_entity
  243. @classmethod
  244. def get_builtin_provider_icon(cls, provider: str) -> tuple[str, str]:
  245. """
  246. get the absolute path of the icon of the builtin provider
  247. :param provider: the name of the provider
  248. :return: the absolute path of the icon, the mime type of the icon
  249. """
  250. # get provider
  251. provider_controller = cls.get_builtin_provider(provider)
  252. absolute_path = path.join(path.dirname(path.realpath(__file__)), 'provider', 'builtin', provider, '_assets',
  253. provider_controller.identity.icon)
  254. # check if the icon exists
  255. if not path.exists(absolute_path):
  256. raise ToolProviderNotFoundError(f'builtin provider {provider} icon not found')
  257. # get the mime type
  258. mime_type, _ = mimetypes.guess_type(absolute_path)
  259. mime_type = mime_type or 'application/octet-stream'
  260. return absolute_path, mime_type
  261. @classmethod
  262. def list_builtin_providers(cls) -> Generator[BuiltinToolProviderController, None, None]:
  263. # use cache first
  264. if cls._builtin_providers_loaded:
  265. yield from list(cls._builtin_providers.values())
  266. return
  267. with cls._builtin_provider_lock:
  268. if cls._builtin_providers_loaded:
  269. yield from list(cls._builtin_providers.values())
  270. return
  271. yield from cls._list_builtin_providers()
  272. @classmethod
  273. def _list_builtin_providers(cls) -> Generator[BuiltinToolProviderController, None, None]:
  274. """
  275. list all the builtin providers
  276. """
  277. for provider in listdir(path.join(path.dirname(path.realpath(__file__)), 'provider', 'builtin')):
  278. if provider.startswith('__'):
  279. continue
  280. if path.isdir(path.join(path.dirname(path.realpath(__file__)), 'provider', 'builtin', provider)):
  281. if provider.startswith('__'):
  282. continue
  283. # init provider
  284. try:
  285. provider_class = load_single_subclass_from_source(
  286. module_name=f'core.tools.provider.builtin.{provider}.{provider}',
  287. script_path=path.join(path.dirname(path.realpath(__file__)),
  288. 'provider', 'builtin', provider, f'{provider}.py'),
  289. parent_type=BuiltinToolProviderController)
  290. provider: BuiltinToolProviderController = provider_class()
  291. cls._builtin_providers[provider.identity.name] = provider
  292. for tool in provider.get_tools():
  293. cls._builtin_tools_labels[tool.identity.name] = tool.identity.label
  294. yield provider
  295. except Exception as e:
  296. logger.error(f'load builtin provider {provider} error: {e}')
  297. continue
  298. # set builtin providers loaded
  299. cls._builtin_providers_loaded = True
  300. @classmethod
  301. def load_builtin_providers_cache(cls):
  302. for _ in cls.list_builtin_providers():
  303. pass
  304. @classmethod
  305. def clear_builtin_providers_cache(cls):
  306. cls._builtin_providers = {}
  307. cls._builtin_providers_loaded = False
  308. @classmethod
  309. def get_tool_label(cls, tool_name: str) -> Union[I18nObject, None]:
  310. """
  311. get the tool label
  312. :param tool_name: the name of the tool
  313. :return: the label of the tool
  314. """
  315. if len(cls._builtin_tools_labels) == 0:
  316. # init the builtin providers
  317. cls.load_builtin_providers_cache()
  318. if tool_name not in cls._builtin_tools_labels:
  319. return None
  320. return cls._builtin_tools_labels[tool_name]
  321. @classmethod
  322. def user_list_providers(cls, user_id: str, tenant_id: str, typ: UserToolProviderTypeLiteral) -> list[UserToolProvider]:
  323. result_providers: dict[str, UserToolProvider] = {}
  324. filters = []
  325. if not typ:
  326. filters.extend(['builtin', 'api', 'workflow'])
  327. else:
  328. filters.append(typ)
  329. if 'builtin' in filters:
  330. # get builtin providers
  331. builtin_providers = cls.list_builtin_providers()
  332. # get db builtin providers
  333. db_builtin_providers: list[BuiltinToolProvider] = db.session.query(BuiltinToolProvider). \
  334. filter(BuiltinToolProvider.tenant_id == tenant_id).all()
  335. find_db_builtin_provider = lambda provider: next(
  336. (x for x in db_builtin_providers if x.provider == provider),
  337. None
  338. )
  339. # append builtin providers
  340. for provider in builtin_providers:
  341. # handle include, exclude
  342. if is_filtered(
  343. include_set=dify_config.POSITION_TOOL_INCLUDES_SET,
  344. exclude_set=dify_config.POSITION_TOOL_EXCLUDES_SET,
  345. data=provider,
  346. name_func=lambda x: x.identity.name
  347. ):
  348. continue
  349. user_provider = ToolTransformService.builtin_provider_to_user_provider(
  350. provider_controller=provider,
  351. db_provider=find_db_builtin_provider(provider.identity.name),
  352. decrypt_credentials=False
  353. )
  354. result_providers[provider.identity.name] = user_provider
  355. # get db api providers
  356. if 'api' in filters:
  357. db_api_providers: list[ApiToolProvider] = db.session.query(ApiToolProvider). \
  358. filter(ApiToolProvider.tenant_id == tenant_id).all()
  359. api_provider_controllers = [{
  360. 'provider': provider,
  361. 'controller': ToolTransformService.api_provider_to_controller(provider)
  362. } for provider in db_api_providers]
  363. # get labels
  364. labels = ToolLabelManager.get_tools_labels([x['controller'] for x in api_provider_controllers])
  365. for api_provider_controller in api_provider_controllers:
  366. user_provider = ToolTransformService.api_provider_to_user_provider(
  367. provider_controller=api_provider_controller['controller'],
  368. db_provider=api_provider_controller['provider'],
  369. decrypt_credentials=False,
  370. labels=labels.get(api_provider_controller['controller'].provider_id, [])
  371. )
  372. result_providers[f'api_provider.{user_provider.name}'] = user_provider
  373. if 'workflow' in filters:
  374. # get workflow providers
  375. workflow_providers: list[WorkflowToolProvider] = db.session.query(WorkflowToolProvider). \
  376. filter(WorkflowToolProvider.tenant_id == tenant_id).all()
  377. workflow_provider_controllers = []
  378. for provider in workflow_providers:
  379. try:
  380. workflow_provider_controllers.append(
  381. ToolTransformService.workflow_provider_to_controller(db_provider=provider)
  382. )
  383. except Exception as e:
  384. # app has been deleted
  385. pass
  386. labels = ToolLabelManager.get_tools_labels(workflow_provider_controllers)
  387. for provider_controller in workflow_provider_controllers:
  388. user_provider = ToolTransformService.workflow_provider_to_user_provider(
  389. provider_controller=provider_controller,
  390. labels=labels.get(provider_controller.provider_id, []),
  391. )
  392. result_providers[f'workflow_provider.{user_provider.name}'] = user_provider
  393. return BuiltinToolProviderSort.sort(list(result_providers.values()))
  394. @classmethod
  395. def get_api_provider_controller(cls, tenant_id: str, provider_id: str) -> tuple[
  396. ApiToolProviderController, dict[str, Any]]:
  397. """
  398. get the api provider
  399. :param provider_name: the name of the provider
  400. :return: the provider controller, the credentials
  401. """
  402. provider: ApiToolProvider = db.session.query(ApiToolProvider).filter(
  403. ApiToolProvider.id == provider_id,
  404. ApiToolProvider.tenant_id == tenant_id,
  405. ).first()
  406. if provider is None:
  407. raise ToolProviderNotFoundError(f'api provider {provider_id} not found')
  408. controller = ApiToolProviderController.from_db(
  409. provider,
  410. ApiProviderAuthType.API_KEY if provider.credentials['auth_type'] == 'api_key' else
  411. ApiProviderAuthType.NONE
  412. )
  413. controller.load_bundled_tools(provider.tools)
  414. return controller, provider.credentials
  415. @classmethod
  416. def user_get_api_provider(cls, provider: str, tenant_id: str) -> dict:
  417. """
  418. get api provider
  419. """
  420. """
  421. get tool provider
  422. """
  423. provider: ApiToolProvider = db.session.query(ApiToolProvider).filter(
  424. ApiToolProvider.tenant_id == tenant_id,
  425. ApiToolProvider.name == provider,
  426. ).first()
  427. if provider is None:
  428. raise ValueError(f'you have not added provider {provider}')
  429. try:
  430. credentials = json.loads(provider.credentials_str) or {}
  431. except:
  432. credentials = {}
  433. # package tool provider controller
  434. controller = ApiToolProviderController.from_db(
  435. provider, ApiProviderAuthType.API_KEY if credentials['auth_type'] == 'api_key' else ApiProviderAuthType.NONE
  436. )
  437. # init tool configuration
  438. tool_configuration = ToolConfigurationManager(tenant_id=tenant_id, provider_controller=controller)
  439. decrypted_credentials = tool_configuration.decrypt_tool_credentials(credentials)
  440. masked_credentials = tool_configuration.mask_tool_credentials(decrypted_credentials)
  441. try:
  442. icon = json.loads(provider.icon)
  443. except:
  444. icon = {
  445. "background": "#252525",
  446. "content": "\ud83d\ude01"
  447. }
  448. # add tool labels
  449. labels = ToolLabelManager.get_tool_labels(controller)
  450. return jsonable_encoder({
  451. 'schema_type': provider.schema_type,
  452. 'schema': provider.schema,
  453. 'tools': provider.tools,
  454. 'icon': icon,
  455. 'description': provider.description,
  456. 'credentials': masked_credentials,
  457. 'privacy_policy': provider.privacy_policy,
  458. 'custom_disclaimer': provider.custom_disclaimer,
  459. 'labels': labels,
  460. })
  461. @classmethod
  462. def get_tool_icon(cls, tenant_id: str, provider_type: str, provider_id: str) -> Union[str, dict]:
  463. """
  464. get the tool icon
  465. :param tenant_id: the id of the tenant
  466. :param provider_type: the type of the provider
  467. :param provider_id: the id of the provider
  468. :return:
  469. """
  470. provider_type = provider_type
  471. provider_id = provider_id
  472. if provider_type == 'builtin':
  473. return (dify_config.CONSOLE_API_URL
  474. + "/console/api/workspaces/current/tool-provider/builtin/"
  475. + provider_id
  476. + "/icon")
  477. elif provider_type == 'api':
  478. try:
  479. provider: ApiToolProvider = db.session.query(ApiToolProvider).filter(
  480. ApiToolProvider.tenant_id == tenant_id,
  481. ApiToolProvider.id == provider_id
  482. ).first()
  483. return json.loads(provider.icon)
  484. except:
  485. return {
  486. "background": "#252525",
  487. "content": "\ud83d\ude01"
  488. }
  489. elif provider_type == 'workflow':
  490. provider: WorkflowToolProvider = db.session.query(WorkflowToolProvider).filter(
  491. WorkflowToolProvider.tenant_id == tenant_id,
  492. WorkflowToolProvider.id == provider_id
  493. ).first()
  494. if provider is None:
  495. raise ToolProviderNotFoundError(f'workflow provider {provider_id} not found')
  496. return json.loads(provider.icon)
  497. else:
  498. raise ValueError(f"provider type {provider_type} not found")
  499. ToolManager.load_builtin_providers_cache()