tools.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. import inspect
  2. import logging
  3. from typing import Awaitable, Callable, get_type_hints
  4. from open_webui.apps.webui.models.tools import Tools
  5. from open_webui.apps.webui.models.users import UserModel
  6. from open_webui.apps.webui.utils import load_tools_module_by_id
  7. from open_webui.utils.schemas import json_schema_to_model
  8. log = logging.getLogger(__name__)
  9. def apply_extra_params_to_tool_function(
  10. function: Callable, extra_params: dict
  11. ) -> Callable[..., Awaitable]:
  12. sig = inspect.signature(function)
  13. extra_params = {
  14. key: value for key, value in extra_params.items() if key in sig.parameters
  15. }
  16. is_coroutine = inspect.iscoroutinefunction(function)
  17. async def new_function(**kwargs):
  18. extra_kwargs = kwargs | extra_params
  19. if is_coroutine:
  20. return await function(**extra_kwargs)
  21. return function(**extra_kwargs)
  22. return new_function
  23. # Mutation on extra_params
  24. def get_tools(
  25. webui_app, tool_ids: list[str], user: UserModel, extra_params: dict
  26. ) -> dict[str, dict]:
  27. tools_dict = {}
  28. for tool_id in tool_ids:
  29. tools = Tools.get_tool_by_id(tool_id)
  30. if tools is None:
  31. continue
  32. module = webui_app.state.TOOLS.get(tool_id, None)
  33. if module is None:
  34. module, _ = load_tools_module_by_id(tool_id)
  35. webui_app.state.TOOLS[tool_id] = module
  36. extra_params["__id__"] = tool_id
  37. if hasattr(module, "valves") and hasattr(module, "Valves"):
  38. valves = Tools.get_tool_valves_by_id(tool_id) or {}
  39. module.valves = module.Valves(**valves)
  40. if hasattr(module, "UserValves"):
  41. extra_params["__user__"]["valves"] = module.UserValves( # type: ignore
  42. **Tools.get_user_valves_by_id_and_user_id(tool_id, user.id)
  43. )
  44. for spec in tools.specs:
  45. # TODO: Fix hack for OpenAI API
  46. for val in spec.get("parameters", {}).get("properties", {}).values():
  47. if val["type"] == "str":
  48. val["type"] = "string"
  49. # Remove internal parameters
  50. spec["parameters"]["properties"] = {
  51. key: val
  52. for key, val in spec["parameters"]["properties"].items()
  53. if not key.startswith("__")
  54. }
  55. function_name = spec["name"]
  56. # convert to function that takes only model params and inserts custom params
  57. original_func = getattr(module, function_name)
  58. callable = apply_extra_params_to_tool_function(original_func, extra_params)
  59. if hasattr(original_func, "__doc__"):
  60. callable.__doc__ = original_func.__doc__
  61. # TODO: This needs to be a pydantic model
  62. tool_dict = {
  63. "toolkit_id": tool_id,
  64. "callable": callable,
  65. "spec": spec,
  66. "pydantic_model": json_schema_to_model(spec),
  67. "file_handler": hasattr(module, "file_handler") and module.file_handler,
  68. "citation": hasattr(module, "citation") and module.citation,
  69. }
  70. # TODO: if collision, prepend toolkit name
  71. if function_name in tools_dict:
  72. log.warning(f"Tool {function_name} already exists in another tools!")
  73. log.warning(f"Collision between {tools} and {tool_id}.")
  74. log.warning(f"Discarding {tools}.{function_name}")
  75. else:
  76. tools_dict[function_name] = tool_dict
  77. return tools_dict
  78. def doc_to_dict(docstring):
  79. lines = docstring.split("\n")
  80. description = lines[1].strip()
  81. param_dict = {}
  82. for line in lines:
  83. if ":param" in line:
  84. line = line.replace(":param", "").strip()
  85. param, desc = line.split(":", 1)
  86. param_dict[param.strip()] = desc.strip()
  87. ret_dict = {"description": description, "params": param_dict}
  88. return ret_dict
  89. def get_tools_specs(tools) -> list[dict]:
  90. function_list = [
  91. {"name": func, "function": getattr(tools, func)}
  92. for func in dir(tools)
  93. if callable(getattr(tools, func))
  94. and not func.startswith("__")
  95. and not inspect.isclass(getattr(tools, func))
  96. ]
  97. specs = []
  98. for function_item in function_list:
  99. function_name = function_item["name"]
  100. function = function_item["function"]
  101. function_doc = doc_to_dict(function.__doc__ or function_name)
  102. specs.append(
  103. {
  104. "name": function_name,
  105. # TODO: multi-line desc?
  106. "description": function_doc.get("description", function_name),
  107. "parameters": {
  108. "type": "object",
  109. "properties": {
  110. param_name: {
  111. "type": param_annotation.__name__.lower(),
  112. **(
  113. {
  114. "enum": (
  115. str(param_annotation.__args__)
  116. if hasattr(param_annotation, "__args__")
  117. else None
  118. )
  119. }
  120. if hasattr(param_annotation, "__args__")
  121. else {}
  122. ),
  123. "description": function_doc.get("params", {}).get(
  124. param_name, param_name
  125. ),
  126. }
  127. for param_name, param_annotation in get_type_hints(
  128. function
  129. ).items()
  130. if param_name != "return"
  131. and not (
  132. param_name.startswith("__") and param_name.endswith("__")
  133. )
  134. },
  135. "required": [
  136. name
  137. for name, param in inspect.signature(
  138. function
  139. ).parameters.items()
  140. if param.default is param.empty
  141. and not (name.startswith("__") and name.endswith("__"))
  142. ],
  143. },
  144. }
  145. )
  146. return specs